What Is a DOCX File? Unzip One and the Text Is Right There
DOCX files are Word documents saved as a ZIP package of XML parts. Jump to your question below, or work through it in order.
What Is a DOCX File? Word's default format since 2007, packed as a ZIP file
Short answer
DOCX files are Word documents saved as a ZIP package of XML parts. Word has made DOCX the default for new documents since Word 2007. The text is in one part, and the pictures, styles and settings are in others.
The signature is shared. DOCX, XLSX, PPTX and ODT files all start with 50 4b 03 04, so the proof is two parts inside the package. In our tests of 28 damaged DOCX files, the best readers returned all the text from 12, and a text pass found all of it in 20.
To check your DOCX file, read the signature test. To open one, read how to open DOCX files. If yours is damaged, start with the damage tests.
DOCX files are Word documents saved as a ZIP package of XML parts. Word has made DOCX the default for new documents since Word 2007, and you can unzip one to read its text.
Picture a contract named offer.docx that Word refuses to open. The file is 80 KB, so something is inside. Rename it to offer.zip and open it, and a folder of XML files appears. That is the idea behind the format, and it is why a broken DOCX file is often easier to save than a broken DOC file.
FileInfo describes DOCX files as documents made by Word or exported from other word processors. They can contain text, images, tables and charts. The Library of Congress says DOCX has been the default in Word for new documents and Save since Word 2007.
The name belongs to a family. The Library of Congress lists .dotx for templates, .docm for documents with macros and .dotm for templates with macros. The kinds section shows how they differ.
Ecma approved the standard, ECMA-376, in December 2006, and ISO/IEC 29500 followed in November 2008, says Wikipedia. Ecma now lists the fifth edition as the current one.
DOCX file facts at a glance
| Property | Value |
|---|---|
| Meaning | Word documents saved as Office Open XML, a ZIP package of XML parts |
| Ending | .docx for documents. Related endings are .dotx, .docm and .dotm, says the Library of Congress. |
| Made by | Microsoft Word. Other word processors can export DOCX files too, says FileInfo. |
| MIME type |
application/
|
| Starts with |
50 4b 03 04, the ZIP signature that XLSX, PPTX and ODT files share
|
| Container | A ZIP package with the folders _rels, docProps and word |
| Text part |
word/, says the Library of Congress
|
| Opens in | Word, LibreOffice, Apple Pages, Google Docs and ONLYOFFICE, says FileInfo |
| Safe to open | Reading the text is low risk. Macros are the risk, and they belong in DOCM files. |
Wrong page? Older Word files that end in .doc are in the DOC file guide. Open-source documents are in the ODT file guide. Read-only documents are in the PDF file guide. Any ZIP archive is in the ZIP file guide.
How Do You Know Your File Is a DOCX File? The first 4 bytes are shared, so read the parts
The first 4 bytes do not prove a DOCX file. DOCX, XLSX, PPTX and ODT files all start with 50 4b 03 04. The proof is inside the package: the content types part names the Word type, and the root relationships point to word/document.xml.
Many pages call 50 4b 03 04 the signature of DOCX files. It is the signature of ZIP. Our own earlier page on this site said the same. We built DOCX files with three writers and saved an Excel file, a PowerPoint file and an ODT file under the ending .docx. All of them start with those 4 bytes.
The file command named our good DOCX files Microsoft Word 2007+. It gave the same name to 23 of our 28 damaged DOCX files, and 14 of those returned nothing from all five readers. The commands unzip -t and 7z t passed only 7 of the 28. So neither tool shows whether the text can be read.
The PRONOM entry identifies DOCX files by more than the first bytes. Its container signature looks inside the ZIP package, as the box below shows. The entries for DOTX templates and DOTM files look for their own content types in the same part.
Look in the ZIP part [Content_Types].xml
Within the first 32,768 bytes of that part
For the text (ASCII, UTF-16LE or UTF-16BE):
application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml
The script below checks DOCX files with the Python standard library. It reads the end record and the central directory of the ZIP, tests the CRC of every part and reads the content types. Then it follows the root relationships to the main part and prints the problems it finds. Save it as check_docx.py and give it the name of your DOCX file.
#!/usr/bin/env python3
"""check_docx.py: look inside a .docx file without opening Word.
Usage: python3 check_docx.py file.docx [more files]
Prints the first 8 bytes, what kind of file it is, the size, the end record and
central directory of the ZIP package, every part with a CRC test, the main
content type (DOCX, template or macro-enabled) and any problems found.
Reads only. It never changes the file."""
import struct, sys, zlib, re
CFB = bytes.fromhex('d0cf11e0a1b11ae1')
TYPES = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml': 'DOCX document',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml': 'DOTX template',
'application/vnd.ms-word.document.macroEnabled.main+xml': 'DOCM macro-enabled document',
'application/vnd.ms-word.template.macroEnabledTemplate.main+xml': 'DOTM macro-enabled template',
}
def read_part(b, off, csize, method):
nl, el = struct.unpack_from('<HH', b, off + 26)
s = off + 30 + nl + el
raw = b[s:s + csize]
if method == 0: return raw
if method == 8: return zlib.decompress(raw, -15)
raise ValueError(f'method {method}')
def central(b):
e = b.rfind(b'PK\x05\x06')
if e < 0: return None, 'no end record (PK 05 06)'
n, size, start = struct.unpack_from('<HII', b, e + 10)
ents, p = [], start
if start + size > e + 1 or b[start:start + 4] != b'PK\x01\x02':
return None, f'end record points to {start:,}, no central directory there'
for _ in range(n):
if b[p:p + 4] != b'PK\x01\x02': return ents, 'central directory is cut short'
meth, = struct.unpack_from('<H', b, p + 10)
crc, cs, us = struct.unpack_from('<III', b, p + 16)
nl, el, cl = struct.unpack_from('<HHH', b, p + 28)
off, = struct.unpack_from('<I', b, p + 42)
ents.append((b[p + 46:p + 46 + nl].decode('utf-8', 'replace'), meth, crc, cs, us, off))
p += 46 + nl + el + cl
return ents, None
def check(path):
b = open(path, 'rb').read()
print(f'{path}: {len(b):,} bytes, starts {b[:8].hex(" ")}')
problems = []
if not b:
print(' empty file'); return ['empty file']
if b[:8] == CFB:
print(' kind: compound file (CFB). A password-protected DOCX looks like this; so does an old DOC.')
try:
import olefile, io
names = ['/'.join(x) for x in olefile.OleFileIO(io.BytesIO(b)).listdir()]
print(' streams:', ', '.join(names))
if 'EncryptedPackage' in names:
print(' this is an encrypted DOCX: it needs its password before it is a ZIP'); return []
if 'WordDocument' in names:
print(' this is an old DOC, not a DOCX'); return ['not a DOCX: old DOC']
return ['not a DOCX: compound file without EncryptedPackage']
except Exception as ex: print(' could not list streams:', ex)
return []
if b[:5] == b'{\\rtf':
print(' kind: RTF text, not a DOCX'); return ['not a ZIP: RTF']
if b[:5] == b'%PDF-':
print(' kind: PDF, not a DOCX'); return ['not a ZIP: PDF']
if b[:4] != b'PK\x03\x04':
at = b[:65536].find(b'PK\x03\x04')
if at > 0:
if b[:4] == bytes(4) and any(b[:at]):
print(f' the first 4 bytes are zeros (the ZIP signature is wiped); the next signature is at offset {at:,}')
problems.append('the first 4 bytes are zeros (signature wiped)')
elif not any(b[:at]):
print(f' the first {at:,} bytes are zeros; the next ZIP signature is at offset {at:,}')
problems.append(f'the first {at:,} bytes are zeros (start of the file wiped)')
else:
print(f' ZIP signature found at offset {at:,}: extra bytes come before the file')
problems.append(f'junk before the file ({at:,} bytes)')
else:
head = b[:512].lstrip().lower()
printable = sum(32 <= x < 127 or x in (9, 10, 13) for x in b[:2000]) / max(1, len(b[:2000]))
if head.startswith((b'<?xml', b'<html', b'<!doctype')): print(' kind: XML or HTML text, not a DOCX')
elif printable > .95: print(' kind: plain text, not a DOCX')
else: print(' kind: no ZIP signature (PK 03 04)')
return problems or ['no ZIP signature']
if b.count(b'\r\n') > 20 and b'\n' not in b.replace(b'\r\n', b''):
problems.append('every 0A is paired with 0D: a text-mode transfer')
ents, err = central(b)
if err: problems.append(err)
else:
e = b.rfind(b'PK\x05\x06'); clen, = struct.unpack_from('<H', b, e + 20)
if e + 22 + clen < len(b): problems.append(f'{len(b) - (e + 22 + clen):,} extra bytes after the end record')
parts = {}
if ents:
print(f' central directory: {len(ents)} entries')
for name, meth, crc, cs, us, off in ents:
st = 'ok'
try:
if b[off:off + 4] != b'PK\x03\x04': st = 'no local header at the stored offset'
else:
d = read_part(b, off, cs, meth)
if zlib.crc32(d) & 0xffffffff != crc: st = 'CRC does not match'
else: parts[name] = d
except Exception as ex: st = 'data is damaged (inflate failed)'
if st != 'ok': problems.append(f'{name}: {st}')
print(f' {name} {us:,} bytes {st}')
else:
# fall back: walk local headers
p, found = 0, 0
while True:
p = b.find(b'PK\x03\x04', p)
if p < 0: break
nl, el = struct.unpack_from('<HH', b, p + 26); meth, = struct.unpack_from('<H', b, p + 8)
cs, = struct.unpack_from('<I', b, p + 18)
name = b[p + 30:p + 30 + nl].decode('utf-8', 'replace'); found += 1
try:
d = zlib.decompressobj(-15).decompress(b[p + 30 + nl + el:]) if meth == 8 else b[p + 30 + nl + el:p + 30 + nl + el + cs]
parts[name] = d; print(f' local header: {name} {len(d):,} bytes')
except Exception as ex: print(f' local header: {name} cannot be read')
p += 4
print(f' no usable central directory; {found} local headers found by scanning')
names = set(parts) | {e[0] for e in (ents or [])}
other = None
if 'xl/workbook.xml' in names: other = 'Excel workbook (XLSX)'
elif 'ppt/presentation.xml' in names: other = 'PowerPoint presentation (PPTX)'
elif 'mimetype' in names:
mt = parts.get('mimetype', b'').decode('utf-8', 'replace').strip()
other = 'OpenDocument file (' + (mt.rsplit('.', 1)[-1] if mt else 'ODF') + ')'
elif names and not any(n.startswith('word/') for n in names): other = 'ZIP archive with no word/ folder'
if other and 'word/document.xml' not in names:
print(f' kind: {other}, not a DOCX')
print(' problems: not a DOCX')
return ['not a DOCX: ' + other]
for need in ('[Content_Types].xml', '_rels/.rels'):
if need not in parts and not any(x.startswith(need + ':') for x in problems): problems.append(f'{need} is missing or unreadable')
ct = parts.get('[Content_Types].xml', b'').decode('utf-8', 'replace')
main = None
for t, label in TYPES.items():
if t in ct: main = label; break
if ct and not main: problems.append('no Word main-part content type in [Content_Types].xml')
if main: print(f' main part content type: {main}')
rels = parts.get('_rels/.rels', b'').decode('utf-8', 'replace')
m = re.search(r'Type="[^"]*officeDocument"[^>]*Target="([^"]+)"|Target="([^"]+)"[^>]*Type="[^"]*officeDocument"', rels)
target = (m.group(1) or m.group(2)).lstrip('/') if m else None
if rels and not target: problems.append('_rels/.rels has no officeDocument relationship')
if target:
print(f' main part: {target}')
if target not in parts:
if not any(x.startswith(target + ':') for x in problems): problems.append(f'{target} is missing or unreadable')
else:
x = parts[target].decode('utf-8', 'replace')
if not x.strip(): problems.append(f'{target} is empty')
elif not re.search(r'</w:document>\s*$', x): problems.append(f'{target} does not end with </w:document>')
else:
try:
import xml.dom.minidom as md; md.parseString(parts[target])
except Exception as ex: problems.append(f'{target} is not well-formed XML')
print(f' text elements (w:t): {len(re.findall(r"<w:t[ >]", x))}')
relp = target.rsplit('/', 1)[0] + '/_rels/' + target.rsplit('/', 1)[-1] + '.rels'
if relp not in parts: problems.append(f'{relp} is missing')
print(' problems:', '; '.join(problems) if problems else 'none found')
return problems
if __name__ == '__main__':
for p in sys.argv[1:]:
try: check(p)
except Exception as ex: print(f' could not check: {ex}')
full.docx: 76,357 bytes, starts 50 4b 03 04 14 00 00 00
central directory: 18 entries
[Content_Types].xml 1,788 bytes ok
_rels/.rels 734 bytes ok
docProps/core.xml 816 bytes ok
docProps/app.xml 1,132 bytes ok
word/document.xml 8,193 bytes ok
word/_rels/document.xml.rels 1,359 bytes ok
word/styles.xml 349,458 bytes ok
word/stylesWithEffects.xml 438,131 bytes ok
word/settings.xml 2,535 bytes ok
word/webSettings.xml 438 bytes ok
word/fontTable.xml 2,811 bytes ok
word/theme/theme1.xml 10,939 bytes ok
customXml/item1.xml 262 bytes ok
customXml/_rels/item1.xml.rels 295 bytes ok
customXml/itemProps1.xml 354 bytes ok
word/numbering.xml 5,513 bytes ok
word/media/image1.png 38,520 bytes ok
docProps/thumbnail.jpeg 8,324 bytes ok
main part content type: DOCX document
main part: word/document.xml
text elements (w:t): 53
problems: none found
We tried the script on all 61 test files. It flagged none of the 25 good DOCX files and all 28 damaged DOCX files, and it named the kind of all 8 files that were not DOCX files. A flipped bit shows up too, because ZIP stores a CRC for every part. The script cannot say which damage matters, and the damage section lists all 28 cases.
broken.docx: 76,869 bytes, starts ed 6f ac 06 1d 49 87 e2
ZIP signature found at offset 512: extra bytes come before the file
local header: [Content_Types].xml 1,788 bytes
local header: _rels/.rels 734 bytes
local header: docProps/core.xml 816 bytes
local header: docProps/app.xml 1,132 bytes
local header: word/document.xml 8,193 bytes
local header: word/_rels/document.xml.rels 1,359 bytes
local header: word/styles.xml 349,458 bytes
local header: word/stylesWithEffects.xml 438,131 bytes
local header: word/settings.xml 2,535 bytes
local header: word/webSettings.xml 438 bytes
local header: word/fontTable.xml 2,811 bytes
local header: word/theme/theme1.xml 10,939 bytes
local header: customXml/item1.xml 262 bytes
local header: customXml/_rels/item1.xml.rels 295 bytes
local header: customXml/itemProps1.xml 354 bytes
local header: word/numbering.xml 5,513 bytes
local header: word/media/image1.png 38,520 bytes
local header: docProps/thumbnail.jpeg 8,324 bytes
no usable central directory; 18 local headers found by scanning
main part content type: DOCX document
main part: word/document.xml
text elements (w:t): 53
problems: junk before the file (512 bytes); end record points to 75,147, no central directory there
| Kind | Where to look | What you see |
|---|---|---|
| XLSX file | The part names |
xl/, not word/
|
| PPTX file | The part names |
ppt/
|
| ODT file | The part names | A part named mimetype |
| Password-protected DOCX | Bytes 0 to 7 |
d0 cf 11 e0 a1 b1 1a e1 and a stream named Encrypted |
| Old DOC file | Bytes 0 to 7 |
The same 8 bytes and a stream named Word |
| RTF file | The first bytes | The text {\rtf1 |
| Web page | The first bytes | The text <!DOCTYPE or <html |
| PDF file | The first bytes | The text %PDF- |
| Plain text file | The entire file | Readable text from byte 0 |
| Empty file | The size | 0 bytes |
Four more clues about your DOCX file
What Is Inside a DOCX File? Parts, relationships and one XML file of text
A DOCX file is a ZIP package of parts. One part lists the content types, small parts link the others together, and the text is in word/document.xml. In our file, the style parts were 90 percent of the unpacked bytes.
Think of the shipping box and its packing slip. The slip, [Content_Types].xml, says what every item is. Smaller slips, called relationships, say which item belongs with which. The text is one item in the box, and the box is a ZIP file.
The sizes in the figure come from one DOCX file we built with python-docx, with 40 paragraphs, a list, a table and a picture. It has 18 parts and 76,357 bytes. Unpacked, the parts add up to 871,602 bytes, and two style parts are 787,589 of them. ZIP shrinks them about 31 to 1. The text part is 8,193 bytes unpacked and 1,562 bytes stored.
The Library of Congress format record says a minimal DOCX package has the folders _rels, docProps and word, plus a content types part, and that the main content is word/document.xml. Microsoft Learn adds that the simplest valid document needs only a main document part.
The parts of one DOCX file
| Part | What it does | Bytes |
|---|---|---|
[Content_
|
Names the kind of every part | 1,788 |
_rels/
|
Points to the main part | 734 |
word/
|
The text, the table and the list | 8,193 |
word/
|
Links the main part to styles, the picture and settings | 1,359 |
word/
|
Style definitions | 349,458 |
word/
|
More style definitions from the python-docx template | 438,131 |
word/
|
The picture, stored as it came | 38,520 |
docProps/
|
Title, author, last editor and times | 816 |
The other 10 parts contain fonts, a theme, settings, numbering, a thumbnail and small custom XML files. Style parts decide the size of a small DOCX file. LibreOffice wrote one styles part of 347,389 bytes, pandoc wrote one of 21,011 bytes, and python-docx wrote the two above. The samples section shows what that did to file sizes.
The text in word/document.xml
Microsoft Learn names the nesting: a document contains a body, and the body contains paragraphs (w:p). Inside a paragraph are pieces of text that share one set of properties (w:r). Inside every piece is the text itself (w:t). Any ZIP tool can show you this part.
<w:p>
<w:pPr><w:pStyle w:val="Heading1"/></w:pPr>
<w:r><w:t>Harbor Notes</w:t></w:r>
</w:p>
<w:p>
<w:r><w:t>Paragraph 01: The harbor report for week 1
lists 52 boats and 255 crates.</w:t></w:r>
</w:p>
Every picture in your DOCX file is a separate part. We pulled word/media/image1.png out with unzip, and its SHA-256 hash matched the PNG we put in. We did not test any DOCX file made by Word, which may store pictures in other ways.
Which part does a reader need?
We removed one part at a time from copies of our full DOCX file and tried five readers. The readers use different shortcuts. That is why one reader can open a file that another refuses, and why a script that writes the missing slips can save a file that no reader would open.
| Part removed | Still read it | Failed |
|---|---|---|
| Content types | pandoc, docx2txt and mammoth | LibreOffice and python-docx |
|
Root relation |
docx2txt and mammoth | LibreOffice, pandoc and python-docx |
| Main part links | All except mammoth | mammoth |
| Main part | None | All five |
Which Kind of DOCX File Do You Have? Documents, templates, macro-enabled files and passwords
DOCX files come in a few kinds: documents, templates, macro-enabled files and password-protected files. The first three differ by one line of text in the content types part. A password wraps the package in a different kind of file.
The ending .docx covers documents. The Library of Congress lists .dotx for templates, .docm for macro-enabled documents and .dotm for macro-enabled templates. Inside any DOCX file, the kind is the content type of its main part, and our check script prints it.
| Kind | How to see it | Our files |
|---|---|---|
| Document | The ending .docx and the document content type | Made 20 files |
| Template | The ending .dotx and the template content type | Made 2 files |
| Macro-enabled document | The ending .docm and the macro-enabled content type | Made 2 files |
| Macro-enabled template | The ending .dotm and its own content type | Not made |
| Password-protected |
A compound file with an Encrypted |
Made 1 file |
| Strict | Other content types, listed by PRONOM as fmt/1827 | Not made |
DOCX application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml
DOTX application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml
DOCM application/vnd.ms-word.document.macroEnabled.main+xml
DOTM application/vnd.ms-word.template.macroEnabledTemplate.main+xml
PRONOM lists the template and macro-enabled template strings on its DOTX page and its DOTM page. Strict files, which PRONOM lists as fmt/1827, use other content types, and we made none.
A macro-enabled copy is still a ZIP file
We made one DOCM file and one DOCM copy under the ending .docx by editing the content type of a DOCX file. Neither contains a macro, so they show only how readers treat the label. Both stopped python-docx with a ValueError that names the content type, and both templates did the same. LibreOffice, pandoc, docx2txt, mammoth and the text pass read all 52 items from all four.
We did not test any DOCX file with a macro. Our macro-enabled files are labels only, so we have no result for code inside a package.
A password-protected DOCX file is not a ZIP file
LibreOffice encrypted the entire package of our test DOCX file. The result is a compound file, as in old DOC files, with two streams and nothing else. Didier Stevens describes the layout, and Microsoft's encryption specification lists the Standard encryption versions as 2.2, 3.2 and 4.2.
| Stream | Size and first bytes |
|---|---|
|
Encrypted |
59,480 bytes. Starts with 42 e8 00 00, which is 59,458 as a little endian number
|
|
Encryption |
224 bytes. Starts with 03 00 02 00, the version numbers 3 and 2
|
The number 59,458 is the size of the package inside. With the right password, msoffcrypto-tool 6.0.0 wrote one DOCX file of exactly 59,458 bytes, and all five readers then returned all 52 items. With a wrong password it stopped with an InvalidKeyError. The file command named the locked file CDFV2 Encrypted, and unzip said it could not find a zipfile directory.
import io, msoffcrypto
f = msoffcrypto.OfficeFile(open('locked.docx', 'rb'))
f.load_key(password='your-password')
out = io.BytesIO()
f.decrypt(out)
open('unlocked.docx', 'wb').write(out.getvalue())
We do not cover password guessing. We did not test any tool that finds a lost password. If you forgot the password, ask the person who sent the file, or look for an older copy without one.
What Do 61 Test DOCX Files Show? 25 good files, 8 other kinds, 28 damaged copies and five readers
LibreOffice, pandoc, docx2txt, mammoth and a text pass returned all the text from all 19 good DOCX files that have text. Damage is where they differ: a text pass found all the text in 20 of 28 damaged DOCX files, and the best single reader found it in 10.
We found no public set of DOCX files with known content, and files from offices have text we cannot compare. So we built our own in October 2026. Every full DOCX file got the same 52 test items: 40 paragraphs, 9 table cells and 3 list items. A reader counted as returning all the text only when all 52 items came back.
We built 25 good DOCX files with three writers and made 28 damaged DOCX files in 28 ways. We also saved 8 other kinds of file under the ending .docx. We tried five readers on all 61 files: LibreOffice 24.2.7.2, pandoc 3.1.3, python-docx 1.2.0, docx2txt 0.9 and mammoth 1.13.0. A text pass script, GNU strings 2.42, unzip 6.00, 7-Zip 23.01, the file command 5.45 and zip 3.0 helped with the rest.
We did not test any DOCX file made by Word. Our files came from python-docx, LibreOffice and pandoc. We did not use Word, Pages, Google Docs or WPS Office. The DOCM files and one of the two DOTX files came from editing the content type of a DOCX file, so none of them contains a macro. We also left out Strict files, files with embedded objects and the SysTools and Freeviewer tools. Word may write the parts in another order and with other content.
The 25 good DOCX files we built
| Writer | Files | What they test |
|---|---|---|
| python-docx 1.2.0 | 7 files | text, full, multi, big, empty, pic and tableonly. The full DOCX file is 76,357 bytes. |
| LibreOffice 24.2.7.2 | 9 files | The same 7, a template and a password-protected file. The full DOCX file is 59,697 bytes. |
| pandoc 3.1.3 | 6 files | The same set without pic. The full DOCX file is 49,783 bytes. |
| Our edits | 3 files | The full DOCX file with its content type edited to DOCM or DOTX, and a DOCM kept under .docx. |
Five of the 25 files have no text: three empty files and two with only a picture. One has a password. Every reader returned nothing from those, which is right for the first five. So the counts below use the 19 files that have text and no password.
| Reader | Files read | Note |
|---|---|---|
| LibreOffice 24.2.7.2 | 19 of 19 | Failed only on the password-protected file. It also read the ODT, RTF, HTML, text and old DOC files that we had saved as .docx. |
| pandoc 3.1.3 | 19 of 19 | Read every good DOCX file. It read none of the 8 other kinds, even the ones that LibreOffice read. |
| python-docx 1.2.0 | 15 of 19 | Refused both templates and both macro-enabled copies because of their content types. |
| docx2txt 0.9 | 19 of 19 | Read every good DOCX file, with tables. |
| mammoth 1.13.0 | 19 of 19 | Read every good DOCX file, with tables. |
| Text pass | 19 of 19 | Our short script. It needs no word processor. |
| strings 2.42 | 0 of 19 | Found nothing, because ZIP packs the text. It stood in for Notepad. |
The same text in three sizes
Identical content gave DOCX files of widely different sizes. An empty document was 9,700 bytes from pandoc, 19,502 from LibreOffice and 36,563 from python-docx. The writer, not the text, sets the size of a small DOCX file, and style parts are the reason. With 4,000 lines the gap closes, because text starts to count.
| Input | python-docx |
Libre |
pandoc |
|---|---|---|---|
| empty | 36,563 | 19,502 | 9,700 |
| text | 37,135 | 20,110 | 10,347 |
| multi | 36,846 | 19,808 | 10,044 |
| table only | 36,784 | 19,948 | 10,000 |
| full | 76,357 | 59,697 | 49,783 |
| big | 80,054 | 66,025 | 55,970 |
The DOC guide compares sizes of the older format with the same content.
The 28 damaged DOCX files
We damaged DOCX files of our full DOCX file in 28 ways, with one kind of damage in every case. The list of 28 cases gives a fix for every one. The table counts cases where at least one reader returned all 52 items, and cases where the text pass did.
| Damage (cases) | Before | After | Text pass |
|---|---|---|---|
| Start of the file wiped (2) | 2 | 2 | 2 |
| End record or directory wiped or wrong (4) | 1 | 4 | 4 |
| Cut short (5) | 0 | 5 | 5 |
| Extra bytes or a bad transfer (4) | 2 | 4 | 2 |
| Flipped bits (3) | 2 | 2 | 2 |
| A part removed (4) | 3 | 3 | 3 |
| A part damaged or empty (5) | 2 | 2 | 2 |
| Empty file (1) | 0 | 0 | 0 |
| All 28 cases | 12 | 22 | 20 |
Before repair means at least one of five readers returned all 52 items from a damaged copy. After repair means the same check on output from our repair script. The text pass is a short script with no repair step. The repair script added 10 cases to what readers managed alone. The text pass beat every single reader, and docx2txt came closest with 10 cases.
Read the counts with care. We damaged small DOCX files with simple breaks. Failing drives, bad downloads and half-finished copies do messier harm. The numbers show how these readers behave with these files, and they are not rates for all DOCX files.
What Does Our Expert Say About DOCX Files? A note from Neeraj Singh on getting the text out first
Pull the text out of a damaged DOCX file before you try to repair it. In our tests, a text pass found all the text in 20 of 28 damaged DOCX files, and the best single reader found it in 10.
Expert insight
“A DOCX file is a box of small files, so damage rarely takes everything. In our tests, a text pass found all the text in 20 of 28 damaged DOCX files, and the best single reader found it in 10. Work on a copy, pull the text out first and only then try to repair the package.”
Why the text survives
Every part of any DOCX file has its own header and its own CRC, and the ZIP directory at the end is only an index. When your DOCX file loses its start, its end record or its directory, the parts are often still in place. A text pass that reads local headers finds them. Most readers we tried need the index, so they refused those files.
A text pass is not a repair. Formatting, tables and pictures are gone. It also ignores the CRC, so it reads parts that a strict reader refuses. It found all 52 items even in the copy with a wrong CRC for document.xml, and that is one reason to compare the result with a good copy.
Microsoft's page on damaged documents points the same way. It lists Recover Text from Any File, which keeps the text and loses formatting, graphics and fields. We did not test Word, so we cannot say how it would do on our DOCX files.
Our repair script gave all 52 items back in 22 of 28 cases and part of them in one. It failed in 5: flipped bits in document.xml, zeroed bytes in it, a removed or empty document.xml and an empty file. In all five DOCX copies, the text part itself was gone or broken, and only a good copy helps.
Keep the original. Work on copies of DOCX files and keep the first one. If a repair goes wrong, you can start again. Our own repair script never touches the original and wrote 23 new files.
How Do You Open DOCX Files? Word, LibreOffice, command line readers and passwords
Double-click the DOCX file to open it in Word. Without Word, open it in LibreOffice, or read it with pandoc, docx2txt, mammoth or one unzip command. All of them returned the text from all 19 good DOCX files that have text. A file with a password needs the password first.
A good DOCX file is easy to open. Most trouble comes from a file that is not what its ending says, a locked file or a damaged file. The sections below cover those three cases in order.
| Program | What it does | What we saw |
|---|---|---|
| Microsoft Word | The program that made DOCX the default format in Word 2007 | Not tested |
| LibreOffice | Free office suite for Windows, Mac and Linux | 19 of 19 files |
| Apple Pages and Preview | Listed by FileInfo for Mac | Not tested |
| Google Docs | Opens DOCX files in a browser, says FileInfo | Not tested |
| ONLYOFFICE | Listed by FileInfo as a DOCX editor | Not tested |
| pandoc | Command line converter that writes text, Markdown and more | 19 of 19 files |
| docx2txt | Python module that returns the text | 19 of 19 files |
| mammoth | Python module that returns the text or HTML | 19 of 19 files |
| python-docx | Python module for reading and writing DOCX files | 15 of 19 files |
| unzip and sed | Two standard commands, no word processor | 19 of 19 files |
Open a DOCX file in Word
- Double-click the DOCX file, or open Word and choose File, then Open.
- If Word says the file is corrupt, go to the damaged DOCX files section before you try anything else.
- To get a copy in another format, choose File, then Save As. FileInfo lists DOC, PDF, ODT, RTF, TXT, HTML, XML, MHTML and XPS. We did not test these steps.
Older versions of Word need a free compatibility pack to read and write DOCX files. the Wikipedia page adds that some items, like equations, become pictures in the older program. We did not test it.
Open a DOCX file without Word
LibreOffice returned all the text from all 19 good DOCX files that have text. It also read the ODT, RTF, HTML, text and old DOC files that we had saved as .docx, by content. The first command below writes its text to a file. The others do the same job with other readers.
# one line for every reader we tested
soffice --headless --convert-to "txt:Text (encoded):UTF8" report.docx
pandoc report.docx -o report.txt
python3 -c "import docx2txt; print(docx2txt.process('report.docx'))"
python3 -c "import mammoth; print(mammoth.extract_raw_text(open('report.docx','rb')).value)"
# no tools at all: unzip the text part and strip the tags
unzip -p report.docx word/document.xml | sed 's/<[^>]*>/ /g'
Every reader has a weak spot. The reader python-docx refuses templates and macro-enabled files because of their content types. Neither pandoc, docx2txt nor mammoth read any of the 8 files that were not DOCX files. LibreOffice read 5 of them. The unzip command needs no program at all. It printed every test item from all 19 good DOCX files, including the Hindi, Arabic, Chinese and Russian lines, because document.xml is UTF-8 text. It puts all paragraphs on one long line.
Before you try any of them, list the parts with unzip -l. If the list has no word/document.xml, you do not have a DOCX file, and the parts test says what you do have.
Open a password-protected DOCX file
A password-protected DOCX file is a compound file and not a ZIP file, so zip tools fail on it. In our test, LibreOffice, pandoc, python-docx, docx2txt, mammoth and the text pass all returned nothing from it. You need the password.
With the right password, msoffcrypto-tool wrote a normal DOCX file that all five readers could read. The password part of the kinds section has the code and the numbers.
Free viewers and reference pages
These cards link to six outside pages. Two are Windows or Mac viewers for DOCX files and one is a tool for damaged Word files. The other three are the Library of Congress record, Microsoft's page on damaged documents and the Wikipedia article on the format.
SysTools describes a free desktop viewer, version 3.0, for Windows and Mac. It opens DOCX files only and shows a preview or the text. A paid Pro version exists, and the page says the free version only previews the PDF conversion. The page is not consistent about whether Word must be installed, so read its notes. We did not start it.
Freeviewer describes a Windows viewer, version 4.0, that opens and previews DOCX files made with Office 2007 to 2019 and has a repair option. Its demo only previews recovered files, and its page is not consistent about whether Word is needed. We did not start it.
The SysTools viewer page itself points to the recovery tool for corrupt files. That tool's page says the free trial previews and cannot save. We name no prices, because the pages change.
Tip. You do not need a paid tool to read the words in a damaged DOCX file. The free text pass found all the text in 20 of our 28 damaged DOCX files.
Are DOCX Files Safe to Open? Macros, the internet and what the file says about you
Reading the text of any DOCX file is low risk. Macros are the main danger, and the Library of Congress ties them to .docm and .dotm files. DOCX files also store the author name and save times, so check them before you send a file out.
A DOCX file is a package. It can contain text, pictures, tables, styles and settings. The text and pictures do nothing until you read them. A macro starts working when you choose to enable it, so the safe habit is to leave macros off unless you know the sender.
The ending is a name anyone can change. The content type inside a DOCX file is the better guide, and our check script prints it. We built our test DOCX files with python-docx, LibreOffice and pandoc, so none had a macro. We did not test any DOCX file with a macro, and our DOCM files are labels only.
| Check | What to do | What we saw |
|---|---|---|
| Macros | Read the content type, not the ending | Our script named the DOCX, DOTX and DOCM types |
| Where it came from | Windows Word blocks macros in files from the internet | Microsoft page, not tested |
| Author and times |
Read docProps/ |
Printed our test author |
| Program name | Do not treat it as proof | Said Word for Mac, but Word never touched the file |
| Hidden text | Remove it with Document Inspector | Microsoft page, not tested |
| The file kind | Use the signature test | Named all 8 files that were not DOCX files |
What Microsoft says about macros and the internet
Microsoft's page on blocked macros says Windows marks files from the internet with a Mark of the Web. Office on Windows then blocks the macros in those files by default, and users see a SECURITY RISK banner with no Enable content button. The change began in Current Channel version 2206 on July 27, 2022.
The same page says Office on Mac, on the web, on Android and on iOS is not affected, and neither are perpetual versions like Office 2019 and LTSC 2021. It talks about Office files in general.
Microsoft's file block page lists the DOCX, DOTX, DOCM and DOTM endings and Word Flat Open XML as Word types. An administrator can block them from opening or send them to Protected View. If you manage computers, that setting is the one to look at.
What a DOCX file says about you
The part docProps/core.xml stores title, author, last editor, keywords, description and creation and save times. Our full DOCX file had an 816-byte core part. Three lines of Python printed the names and the date, and the unzip command prints the entire part.
import re, zipfile
x = zipfile.ZipFile('report.docx').read('docProps/core.xml').decode()
for tag in ('dc:creator', 'cp:lastModifiedBy', 'dcterms:created'):
print(tag, re.findall('<%s[^>]*>([^<]*)' % tag, x))
dc:creator ['Test Author']
cp:lastModifiedBy ['Test Editor']
dcterms:created ['2026-10-19T08:00:00Z']
The part docProps/app.xml names a program. Ours said Microsoft Macintosh Word, version 14.0000. Word never touched that file. The library python-docx copied the line from its starting template. So the program name in a DOCX file is a label, and anyone can copy it.
Microsoft's page on Document Inspector says the inspector can remove comments, revision marks, document properties, personal information, headers, footers, watermarks and hidden text. It adds that it cannot find hidden text made in other ways, like white text on a white background. We did not test it.
Do not enable content for strangers. If a DOCX file from someone you do not know asks you to enable editing or enable content, close it. Text you can read without clicking anything is safe to read.
Why Will Your DOCX File Not Open? Twenty-eight damage tests, and the repairs that worked
DOCX files that will not open usually have a damaged ZIP index or a missing part, and the text is usually still inside. In our 28 damaged cases, a reader returned all the text from 12 as they were and from 22 after our repair script. A text pass found all of it in 20.
We broke copies of the full DOCX file in 28 ways and tried the readers before and after repairs. In the chart, green means all 52 items came back, amber means some did and red means none did.
| Damage (cases) | What you see and the fix |
|---|---|
| Start of the file wiped (2) | Only docx2txt read the copies. The text pass found all 52 items in both. Fix: Write the ZIP signature back, or rebuild from the local headers. |
| End record or directory wiped or wrong (4) | unzip and 7-Zip failed on all four. All five readers refused three of them, and only pandoc read the copy with the wrong offset. The text pass found all 52 items in every copy. Fix: Rebuild the index from the local headers. |
| Cut short (5) | All five readers refused every cut file, even the one cut at 90 percent. The text pass found all 52 items in all five. Fix: Download again, or rebuild and lose the picture. |
| Extra bytes or a bad transfer (4) | Junk in front: three readers read it. Junk at the end: four read it, and LibreOffice refused it. A text-mode transfer and a UTF-8 resave: all five refused them. Fix: Cut the junk and change 0D 0A back to 0A, or convert the bytes back. |
| Flipped bits (3) | Bits in document.xml: all five readers refused it. Bits in the picture: python-docx refused it and four read it. Bits in styles.xml: three read it. Fix: Use a good copy. The script keeps the text when the bits are in the picture or styles. |
| A part removed (4) | Without the content types or the root relationships, some readers still read the file (see the table in the inside section). Without document.xml, all five refused it. Both unzip and 7-Zip passed all four. Fix: Write the missing small part again, or use a good copy. |
| A part damaged or empty (5) | Zeroed bytes in document.xml: all five refused it. A wrong CRC: only pandoc read it. 4,096 zeroed bytes mid-file: four read it. A cut document.xml or an empty one: all five refused them. Fix: Keep the full paragraphs of a cut part and ignore a wrong CRC, or use a good copy. |
| Empty file (1) | Nothing to read. Fix: Download again. |
The repairs, in small patches
Always work on a copy. The dd option conv=notrunc matters: without it, dd cuts the file after the bytes it writes and destroys the rest. The four patches below fixed our signature, junk in front, junk at the end and text-mode cases. In all four, the fixed copy was byte for byte the same as the file we started from.
# work on copies, never on the original
# 1. first 4 bytes zeroed: write the ZIP signature back
cp broken.docx fixed-1.docx
printf '\x50\x4b\x03\x04' | dd of=fixed-1.docx bs=1 seek=0 conv=notrunc
# 2. junk in front: check_docx.py said the ZIP signature is at offset 512
tail -c +513 broken.docx > fixed-2.docx
# 3. junk at the end: cut the last 1,024 bytes
head -c -1024 broken.docx > fixed-3.docx
# 4. text-mode transfer: change every 0D 0A back to 0A
python3 -c "d=open('broken.docx','rb').read().replace(b'\r\n',b'\n'); open('fixed-4.docx','wb').write(d)"
The script below does the rest. It also undoes a UTF-8 resave and rebuilds the ZIP index from the local headers. When a small part is missing, it writes the part again from standard rules. When document.xml is cut short, it keeps the full paragraphs. It prints every fix it makes, writes a new file and never touches the original.
#!/usr/bin/env python3
"""repair_docx.py: small, safe fixes for a damaged .docx. Writes a new file, never touches the original.
Usage: python3 repair_docx.py damaged.docx fixed.docx
Steps, in order (every one is printed):
1. undo a text-mode transfer (0D 0A back to 0A) when every 0A has a 0D in front of it
2. cut junk in front of the first ZIP signature, and bytes after the end record
3. if the ZIP still does not open: scan for local headers, inflate every part, keep the parts that pass
the CRC test, and write a fresh ZIP with a new central directory
4. put back a missing [Content_Types].xml, _rels/.rels or word/_rels/document.xml.rels from standard rules
5. if word/document.xml is cut short or damaged, keep every full paragraph before the damage
It cannot bring back text that was never stored, a deflate stream after a flipped bit, or a password."""
import struct, sys, zlib, re, io, zipfile
from xml.sax.saxutils import escape
NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
MAIN = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
OV = {
'word/styles.xml': ('wordprocessingml.styles+xml', 'styles'),
'word/numbering.xml': ('wordprocessingml.numbering+xml', 'numbering'),
'word/settings.xml': ('wordprocessingml.settings+xml', 'settings'),
'word/fontTable.xml': ('wordprocessingml.fontTable+xml', 'fontTable'),
'word/webSettings.xml': ('wordprocessingml.webSettings+xml', 'webSettings'),
'word/footnotes.xml': ('wordprocessingml.footnotes+xml', 'footnotes'),
'word/endnotes.xml': ('wordprocessingml.endnotes+xml', 'endnotes'),
'word/comments.xml': ('wordprocessingml.comments+xml', 'comments'),
}
IMG = {'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'emf': 'image/x-emf', 'wmf': 'image/x-wmf', 'bmp': 'image/bmp'}
def inflate_partial(raw):
"""Inflate as far as the data allows. Returns (bytes, finished_cleanly)."""
d, out = zlib.decompressobj(-15), b''
try:
for i in range(0, len(raw), 64):
out += d.decompress(raw[i:i + 64])
if d.eof: return out, True
except zlib.error:
return out, False
return out, d.eof
def scan_parts(b):
"""Walk the local headers. Returns {name: (data, status)} with status ok, bad-crc or partial."""
parts, p = {}, 0
while True:
p = b.find(b'PK\x03\x04', p)
if p < 0 or p + 30 > len(b): break
flag, meth = struct.unpack_from('<HH', b, p + 6)
crc, cs, us = struct.unpack_from('<III', b, p + 14)
nl, el = struct.unpack_from('<HH', b, p + 26)
name = b[p + 30:p + 30 + nl].decode('utf-8', 'replace')
s = p + 30 + nl + el
if not re.fullmatch(r'[\w\[\]\-. /]+', name) or nl == 0:
p += 4; continue
if meth == 0:
data = b[s:s + cs] if not flag & 8 else b[s:s + us]
st = 'ok' if len(data) == us and (zlib.crc32(data) & 0xffffffff) == crc else 'partial'
if flag & 8 and st == 'partial': st = 'ok'
elif meth == 8:
data, fin = inflate_partial(b[s:s + cs] if cs and not flag & 8 else b[s:])
st = ('ok' if (zlib.crc32(data) & 0xffffffff) == crc or flag & 8 else 'bad-crc') if fin else 'partial'
else:
p += 4; continue
if name not in parts or st == 'ok': parts[name] = (data, st)
p = s
return parts
def paragraphs_before_damage(xml):
t = xml.decode('utf-8', 'replace')
out = []
for m in re.finditer(r'<w:p[ >].*?</w:p>', t, re.S):
txt = ''.join(re.findall(r'<w:t[^>]*>([^<]*)</w:t>', m.group(0)))
out.append(txt)
return out
def plain_document(paras):
body = ''.join(f'<w:p><w:r><w:t xml:space="preserve">{p}</w:t></w:r></w:p>' for p in paras)
return (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:document xmlns:w="{NS_W}"><w:body>{body}'
'<w:sectPr><w:pgSz w:w="12240" w:h="15840"/></w:sectPr></w:body></w:document>').encode()
def content_types(names):
x = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>',
'<Default Extension="xml" ContentType="application/xml"/>']
exts = {n.rsplit('.', 1)[-1].lower() for n in names if '.' in n.rsplit('/', 1)[-1]}
for e in sorted(exts & set(IMG)): x.append(f'<Default Extension="{e}" ContentType="{IMG[e]}"/>')
x.append(f'<Override PartName="/word/document.xml" ContentType="{MAIN}"/>')
for n, (t, _) in OV.items():
if n in names: x.append(f'<Override PartName="/{n}" ContentType="application/vnd.openxmlformats-officedocument.{t}"/>')
if 'word/theme/theme1.xml' in names: x.append('<Override PartName="/word/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>')
if 'word/stylesWithEffects.xml' in names: x.append('<Override PartName="/word/stylesWithEffects.xml" ContentType="application/vnd.ms-word.stylesWithEffects+xml"/>')
for n in sorted(names):
if re.fullmatch(r'customXml/itemProps\d+\.xml', n): x.append(f'<Override PartName="/{n}" ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml"/>')
if 'docProps/core.xml' in names: x.append('<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>')
if 'docProps/app.xml' in names: x.append('<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>')
x.append('</Types>')
return ''.join(x).encode()
def root_rels(names):
R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
x = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
f'<Relationship Id="rId1" Type="{R}/officeDocument" Target="word/document.xml"/>']
if 'docProps/core.xml' in names: x.append('<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>')
if 'docProps/app.xml' in names: x.append(f'<Relationship Id="rId3" Type="{R}/extended-properties" Target="docProps/app.xml"/>')
x.append('</Relationships>')
return ''.join(x).encode()
def doc_rels(names, docxml):
R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
x = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">']
i = 1
for n, (_, rt) in OV.items():
if n in names: x.append(f'<Relationship Id="rId{i}" Type="{R}/{rt}" Target="{n[5:]}"/>'); i += 1
if 'word/theme/theme1.xml' in names: x.append(f'<Relationship Id="rId{i}" Type="{R}/theme" Target="theme/theme1.xml"/>'); i += 1
ids = list(dict.fromkeys(re.findall(r'r:embed="([^"]+)"', docxml.decode('utf-8', 'replace'))))
media = sorted(n for n in names if n.startswith('word/media/'))
for rid, m in zip(ids, media):
x.append(f'<Relationship Id="{rid}" Type="{R}/image" Target="{m[5:]}"/>')
x.append('</Relationships>')
return ''.join(x).encode(), (len(ids), len(media))
def prune(keep, log):
"""Drop relationships and content-type overrides that point at parts which are gone."""
import posixpath
for rp in [n for n in keep if n.endswith('.rels')]:
base = posixpath.dirname(posixpath.dirname(rp))
x = keep[rp].decode('utf-8', 'replace'); gone = []
def chk(m):
tag = m.group(0)
if 'TargetMode="External"' in tag: return tag
t = re.search(r'Target="([^"]+)"', tag)
if not t: return tag
tgt = t.group(1).lstrip('/') if t.group(1).startswith('/') else posixpath.normpath(posixpath.join(base, t.group(1)))
if tgt in keep: return tag
gone.append(tgt); return ''
y = re.sub(r'<Relationship\b[^>]*/>', chk, x)
if gone:
keep[rp] = y.encode(); log.append(f'{rp}: removed {len(gone)} link(s) to missing parts ({", ".join(gone)})')
ct = keep['[Content_Types].xml'].decode('utf-8', 'replace')
ct2 = re.sub(r'<Override PartName="/([^"]+)"[^>]*/>', lambda m: m.group(0) if m.group(1) in keep else '', ct)
if ct2 != ct: keep['[Content_Types].xml'] = ct2.encode(); log.append('[Content_Types].xml: removed overrides for missing parts')
def zip_opens(b):
try:
z = zipfile.ZipFile(io.BytesIO(b)); return z.testzip() is None
except Exception:
return False
def xml_ok(d):
try:
import xml.dom.minidom as md; md.parseString(d); return True
except Exception:
return False
def fix(b):
log = []
if b[:4] == b'PK\x03\x04' and not zip_opens(b):
try:
u = b.decode('utf-8').encode('latin-1')
if zip_opens(u): b = u; log.append('converted the bytes back from UTF-8 to single bytes (undo a UTF-8 resave)')
except Exception:
pass
if b[:4] == b'PK\x03\x04' and b.count(b'\r\n') > 20 and b'\n' not in b.replace(b'\r\n', b''):
b = b.replace(b'\r\n', b'\n'); log.append('replaced 0D 0A with 0A (undo a text-mode transfer)')
at = b[:65536].find(b'PK\x03\x04')
if at > 0 and b[:4] != b'PK\x03\x04' and any(b[:at]) and b[:4] != bytes(4):
b = b[at:]; log.append(f'removed {at} bytes in front of the first ZIP signature')
e = b.rfind(b'PK\x05\x06')
if e >= 0 and e + 22 <= len(b):
clen, = struct.unpack_from('<H', b, e + 20)
if e + 22 + clen < len(b):
n = len(b) - (e + 22 + clen); b = b[:e + 22 + clen]; log.append(f'removed {n} bytes after the end record')
parts = scan_parts(b)
keep, dropped = {}, []
for n, (d, st) in parts.items():
if st == 'ok': keep[n] = d
elif st == 'bad-crc' and (n.startswith('word/media/') or xml_ok(d)):
keep[n] = d; log.append(f'{n}: CRC does not match but the data inflates{"" if n.startswith("word/media/") else " and parses as XML"}; kept it, check the result')
elif st == 'partial' and n.startswith('word/media/'):
keep[n] = d; log.append(f'{n}: cut short; kept the part so the link still points somewhere (the picture will not show)')
else: dropped.append(f'{n} ({st})')
if dropped: log.append('dropped damaged parts: ' + ', '.join(dropped))
names = set(keep)
doc = keep.get('word/document.xml')
if not doc and 'word/document.xml' in parts:
data, st = parts['word/document.xml']
paras = paragraphs_before_damage(data) if data else []
if paras:
doc = plain_document(paras); log.append(f'word/document.xml was {st}: kept {len(paras)} complete paragraphs as plain text (formatting and tables are lost)')
keep['word/document.xml'] = doc; names.add('word/document.xml')
if not doc:
log.append('word/document.xml could not be recovered: there is no text left to rebuild from')
return None, log, 'no document part'
if not re.search(rb'</w:document>\s*$', doc) or not xml_ok(doc):
paras = paragraphs_before_damage(doc)
if not paras:
log.append('word/document.xml is damaged and has no full paragraph'); return None, log, 'no document text'
keep['word/document.xml'] = doc = plain_document(paras); log.append(f'word/document.xml did not end cleanly: rebuilt from {len(paras)} complete paragraphs (formatting and tables are lost)')
rebuilt = False
if '[Content_Types].xml' not in keep:
keep['[Content_Types].xml'] = content_types(names); log.append('wrote a new [Content_Types].xml from standard rules'); rebuilt = True
if '_rels/.rels' not in keep:
keep['_rels/.rels'] = root_rels(names); log.append('wrote a new _rels/.rels from standard rules'); rebuilt = True
if 'word/_rels/document.xml.rels' not in keep:
r, (ni, nm) = doc_rels(names, doc); keep['word/_rels/document.xml.rels'] = r
log.append(f'wrote a new word/_rels/document.xml.rels (picture links are a guess: {ni} used, {nm} stored)'); rebuilt = True
prune(keep, log)
heavy = bool(dropped or rebuilt or any('CRC does not match' in l or 'cut short' in l or 'did not end' in l or 'complete paragraphs' in l or 'missing parts' in l for l in log))
if not heavy and zip_opens(b):
return b, log, ('fixed without rewriting any part' if log else 'nothing to fix: the ZIP opens and every required part is there')
bio = io.BytesIO(); zo = zipfile.ZipFile(bio, 'w', zipfile.ZIP_DEFLATED)
order = ['[Content_Types].xml', '_rels/.rels'] + [n for n in keep if n not in ('[Content_Types].xml', '_rels/.rels')]
for n in order: zo.writestr(n, keep[n])
zo.close()
log.insert(0, f'scanned local headers: {len(parts)} parts found, {sum(1 for d, st in parts.values() if st == "ok")} pass the CRC test')
return bio.getvalue(), log, 'new ZIP written'
if __name__ == '__main__':
b = open(sys.argv[1], 'rb').read()
if not b: print('empty file: nothing to repair'); sys.exit(1)
out, log, note = fix(b)
for l in log: print('fix:', l)
print(note)
if out is None: sys.exit(1)
open(sys.argv[2], 'wb').write(out)
print(f'wrote {sys.argv[2]} ({len(out):,} bytes)')
fix: removed 512 bytes in front of the first ZIP signature
fixed without rewriting any part
wrote fixed.docx (76,357 bytes)
The script wrote 23 files from our 28 copies. In 4 cases the fixed DOCX file matched the original byte for byte. In the others it wrote a new ZIP with the same text. It wrote no file when the text part was gone. When it writes picture links again, it has to guess which part is the picture, so check the result.
What the tools could not fix
Check the result. After a repair, compare the text with the original if you have it. An open file is not always the right file, so count the pages, the table cells and the pictures.
How Do You Get the Text Out of a Broken DOCX File? Two commands and a short script
Unzip a DOCX file and strip the tags, and you have its text. When the ZIP index is damaged, a short script that reads local headers returned all the text from 20 of our 28 damaged DOCX files. The best single reader managed 10.
A DOCX file keeps its text in one XML part, so you can skip the word processor. The one-line command below needs a ZIP index that works. On our 28 damaged DOCX files it printed all 52 items from 11, and part of them from 3 more.
unzip -p broken.docx word/document.xml | sed 's/<[^>]*>/ /g' > text.txt
When unzip fails, use a text pass. The script below ignores the index. It looks for every local header, inflates what it can of every part and prints text from every paragraph. In our tests it returned all 52 items from 20 DOCX copies, part of them from 2 and none from 6. It reads text in stored order, so paragraphs come out in order.
#!/usr/bin/env python3
"""text_pass.py: print the text of a DOCX file, even when the ZIP directory is damaged.
Usage: python3 text_pass.py damaged.docx > text.txt
It ignores the central directory. It scans for local headers, inflates what it
can of every part and prints the w:t text of every paragraph it finds."""
import re, struct, sys, zlib
def parts(b):
p = 0
while True:
p = b.find(b'PK\x03\x04', p)
if p < 0 or p + 30 > len(b): return
meth, = struct.unpack_from('<H', b, p + 8)
nl, el = struct.unpack_from('<HH', b, p + 26)
name = b[p + 30:p + 30 + nl].decode('utf-8', 'replace')
start = p + 30 + nl + el
if meth == 8 and name.endswith('.xml'):
d, out = zlib.decompressobj(-15), b''
try:
for i in range(start, len(b), 64):
out += d.decompress(b[i:i + 64])
if d.eof: break
except zlib.error:
pass
yield name, out
p += 4
for path in sys.argv[1:]:
b = open(path, 'rb').read()
for name, xml in parts(b):
t = xml.decode('utf-8', 'replace')
if '<w:t' not in t: continue
for para in re.findall(r'<w:p[ >].*?(?:</w:p>|$)', t, re.S):
line = ''.join(re.findall(r'<w:t[^>]*>([^<]*)', para))
if line: print(line)
$ python3 text_pass.py broken.docx | wc -l
53
$ python3 text_pass.py broken.docx | head -2
Harbor Notes
Paragraph 01: The harbor report for week 1 lists 52 boats and 255 crates.
The 53 lines are the title and our 52 test items. In that copy, unzip and 7-Zip failed and every reader refused the file.
The limits of a text pass
The text pass drops formatting and pictures, and it prints table cells as separate lines. It reads only what it can inflate. Where a flipped bit or zeroed bytes are early in document.xml, it printed nothing, though the unzip command printed some garbled items from the same copies. It ignores the CRC, so a part with a wrong CRC can give wrong words. Compare the result with a good copy when you have one.
A free ZIP repair: zip -FF
Info-ZIP's zip 3.0 has a repair mode that scans for local headers and writes a new index. It asks whether the archive is a single disk, so answer y.
zip -FF broken.docx --out fixed.docx
# answer y when it asks about a single-disk archive
unzip -t fixed.docx
On our 28 copies, at least one reader returned all 52 items from 20 of the repaired DOCX files. That is 8 more than before repair, and none fewer. All five readers did so for only 7 files, because zip -FF does not write back a missing content types part or relationships part. It did nothing for a text-mode transfer, a UTF-8 resave, bit flips in document.xml, zeroed bytes, a cut or missing document.xml or an empty file. Our own script reached 22 files.
The Register's 2014 guide describes a manual route: rename your DOCX file to .zip, find the unclosed tag in document.xml, fix it and zip the file again. That route works when one tag is wrong and the rest of the XML is fine. It does not help with a broken index or a cut file.
Work on a copy. Every command on this page that writes a file writes a new one. Keep the first copy until you have checked the output.
How Do You Convert a DOCX File to Another Format? PDF, ODT, DOC, RTF, HTML, Markdown and EPUB, checked by count
Convert DOCX files with Save As in Word, or from the command line with LibreOffice or pandoc. In our tests, both kept all 52 items in every format we tried, and ODT, DOC and EPUB kept the picture byte for byte.
Converting any DOCX file reads its document and writes it again in another format. A good conversion keeps words, tables and pictures. We converted our 76,357-byte full DOCX file with LibreOffice 24.2.7.2 and pandoc 3.1.3, and counted the 52 items in every output.
LibreOffice conversions
| Format | Bytes | What we saw |
|---|---|---|
| DOC | 66,048 | All 52 items. The picture was byte for byte the same. |
| ODT | 69,873 | All 52 items. The picture was byte for byte the same. |
| DOTX | 59,701 | All 52 items. A template with the same picture. |
| 40,100 | All 52 items, read back with pdftotext. The picture was stored as a 9,677-byte JPEG. | |
| RTF | 112,396 | All 52 items. Bigger than the DOCX file. |
| HTML | 8,416 | All 52 items. The picture came out as a separate 38,520-byte PNG. |
| Text | 3,114 | All 52 items. Formatting and the picture are gone. |
pandoc conversions
| Format | Bytes | What we saw |
|---|---|---|
| ODT | 46,741 | All 52 items. The picture was byte for byte the same. |
| EPUB | 44,220 | All 52 items. The picture was byte for byte the same. |
| RTF | 82,854 | All 52 items. |
| HTML | 3,843 | All 52 items. No picture came out. |
| Markdown | 3,258 | All 52 items. No picture came out. |
| Text | 3,492 | All 52 items. No picture came out. |
soffice --headless --convert-to pdf report.docx
soffice --headless --convert-to odt report.docx
soffice --headless --convert-to doc report.docx
soffice --headless --convert-to rtf report.docx
soffice --headless --convert-to html report.docx
soffice --headless --convert-to "txt:Text (encoded):UTF8" report.docx
pandoc report.docx -o report.md
pandoc report.docx -o report.epub
PDF was the smallest output with a picture and a fixed layout, and it cannot be edited as a document. Text and Markdown were smaller still, and they lose the formatting. We did not test Word, so we cannot say how its Save As differs.
Count the output
A count shows whether a conversion did its job. We searched every output for all 52 items. Do the same with your DOCX files by comparing page counts, table cells and pictures with the original.
Does the picture survive?
In ODT, DOC, DOTX and EPUB outputs, the picture was stored as the same 38,520 bytes as the PNG we put in. In PDF it was packed again as a JPEG. In LibreOffice HTML it came out as a separate PNG file next to the page, so move both files together. We did not check the picture in RTF files.
Where every format is covered
FileInfo lists these Save As targets in Word: DOC, PDF, ODT, RTF, TXT, HTML, XML, MHTML and XPS, with XPS on Windows only. Read about the PDF format, OpenDocument text, MHTML web archives, XPS documents and EPUB books before you select a target.
Keep the DOCX file. Convert a copy. If the output looks wrong, the DOCX file is the only place the original layout lives.
What Else Looks Like a DOCX File? .dotx, .docm, .zip and endings that mislead
Not every file named .docx is a DOCX file. We saved eight other kinds under the ending .docx. LibreOffice read five of them by their content, and the check script named all eight. Of the other readers, pandoc, python-docx, docx2txt and mammoth read none.
The ending is a name anyone can change. The Wikipedia entry shows the standard, and the PRONOM record shows what a DOCX file is. This table lists endings that you may meet near .docx.
| Ending | What it is | Our test |
|---|---|---|
| .docx | Word 2007 and later documents, the format in this guide | Tested |
| .dotx | Word templates, listed by PRONOM | Tested |
| .docm | Documents with macros, says the Library of Congress | Label only |
| .dotm | Templates with macros | Not tested |
| .doc | Word 97 to 2003 documents, a compound file | Tested |
| .odt | OpenDocument text, also a ZIP package | Tested |
| .xlsx | Excel workbooks, also a ZIP package | Tested |
| .pptx | PowerPoint slides, also a ZIP package | Tested |
| .rtf | Rich Text Format, readable text with markup | Tested |
| .html | A web page that Word can open | Tested |
| .zip | Any ZIP archive, and the container of every DOCX file | Tested |
Endings that mislead
We gave eight other files the ending .docx: an ODT, an RTF, a web page, a text file, an old DOC, a PDF, an Excel workbook and a PowerPoint file. LibreOffice read the first five by content and returned all 52 items from every one. It read nothing from the PDF, workbook or slides. The file command named all eight correctly.
The first-bytes table shows what every kind looks like. A program that trusts the ending would try to read all eight as Word documents. A program that looks inside would not.
Other Word and Office formats
For older Word files, see the guide to the older DOC format and its DOC versus DOCX comparison. The guide to OpenDocument text covers the open-source format. Older spreadsheets and slides are in the guide to XLS workbooks and the guide to PPT slides. For any ZIP archive, see the guide to ZIP archives.
What Is the DOCX File Format Made Of? The facts we could check, and what we left out
The DOCX format is an open standard, and its main facts are easy to check. These are the facts we could confirm against Ecma, the Library of Congress, PRONOM, Microsoft Learn, Wikipedia, FileInfo and our own 61 test files, plus the points we left out.
| Part | What we could check |
|---|---|
| Standard | Office Open XML, as ECMA-376 and ISO/IEC 29500. Ecma lists the fifth edition as the current one, with Part 2 dated December 2021. |
| Editions | Ecma lists archived editions from December 2006, December 2008, June 2011 and December 2012. Wikipedia still names an older edition as the latest, so we used Ecma. |
| Variants | Transitional and Strict. Office 2010 saves only Transitional, and Office 2013 and later fully support Strict but do not default to it, says Wikipedia. |
| Container | A ZIP package with the folders _rels, docProps and word, says the Library of Congress. |
| Main part |
word/, with document, body, paragraph and text elements, says Microsoft Learn.
|
| MIME type |
application/, says the Library of Congress.
|
| PRONOM | fmt/412 for DOCX, fmt/1827 for Strict, fmt/597 for DOTX and fmt/599 for DOTM. The DOCX rule looks in the content types part. |
| Encryption | A compound file with EncryptionInfo and EncryptedPackage streams. Microsoft Learn lists Standard encryption versions 2.2, 3.2 and 4.2. |
| Text | UTF-8 XML. The Hindi, Arabic, Chinese and Russian lines in our multi files came back through the unzip command. |
| Pictures |
In word/ |
| Properties |
Title, author and times in docProps/ |
About the values we list
The sizes and part lists come from our own files and from the sources named. All of our DOCX files came from python-docx 1.2.0, LibreOffice 24.2.7.2 and pandoc 3.1.3 on Linux in October 2026. Word may write other parts and other values, so treat our numbers as guides, not promises.
The Library of Congress record covers the Transitional form, in editions 1 to 5 of the standard. Wikipedia's history section dates Ecma approval to December 2006 and ISO publication to November 2008. Microsoft Learn says the simplest valid document needs only the main document part, which fits what we saw: some readers still read a file that lacks the small parts.
What we left out
We did not test any DOCX file made by Word. We also left out Strict files, files with macros, files with embedded objects and files with comments or tracked changes. We did not use Word, Pages, Google Docs or WPS Office. Everything about them comes from FileInfo, Wikipedia and Microsoft. The DOCM files and one DOTX file are content type edits.
The SysTools and Freeviewer tools come from their own pages, and we did not start either one. We name no prices. Their pages are not always consistent about free and paid features, so read the limits there.
Our damage tests are simple breaks in small files. We tried five readers, and others may behave differently. Python writes the parts of a small file in a fixed order, so a cut file kept its text. Test our check script, text pass and repair script on copies before you trust them with anything that matters.
Sources checked
For the format and its history, see the Wikipedia article on Office Open XML, the FileInfo page for the DOCX ending and the Ecma page for ECMA-376. The Library of Congress record fdd000397 describes the Transitional form. For identification, see the PRONOM record fmt/412. For layout, see the Microsoft Learn page on document structure. For encryption, see the Microsoft Learn page on encryption and the Didier Stevens post on encrypted documents. For damage and safety, see Microsoft on damaged documents, on blocked macros, on file block and on Document Inspector.
Every test count came from the 61 files and the scripts in our research folder.
Frequently Asked Questions About DOCX Files
What is a DOCX file?
DOCX files are Word documents saved as a ZIP package of XML parts. Word has made DOCX the default for new documents since Word 2007, says the Library of Congress. The text is in word/document.xml, and the pictures, styles and settings are in other parts. FileInfo says a DOCX file can contain text, images, tables and charts.
How do I open a DOCX file?
Double-click it to open it in Word. Without Word, use LibreOffice, Apple Pages, Google Docs or ONLYOFFICE, which FileInfo lists for DOCX files. In our tests, LibreOffice, pandoc, docx2txt, mammoth and an unzip command all returned the text from all 19 good DOCX files that have text.
How do I know if a file is a DOCX file?
The first 4 bytes, 50 4b 03 04, do not prove it, because XLSX, PPTX and ODT files start the same way. Look inside the ZIP for a content types part that names the Word type and for word/document.xml. A short Python script on this page lists the parts, tests every CRC and prints the kind.
Is a DOCX file safe to open?
Reading the text is low risk. Macros are the danger, because they can start code when you enable them, and the Library of Congress ties them to .docm and .dotm files. Microsoft says Office on Windows blocks macros in files from the internet by default. Do not enable content for a file from someone you do not know.
How do I open a password-protected DOCX file?
You need the password. A locked DOCX file is a compound file and not a ZIP file, and all our readers returned nothing from it. With the right password, msoffcrypto-tool decrypted our test file into a normal DOCX file, and all five readers then returned the text. We did not test any way to find a lost password.
Why will my DOCX file not open?
The usual cause is a damaged ZIP index or a missing part. In our 28 damaged DOCX files, a reader returned all the text from 12 as they were and from 22 after our repair script. A text pass found all of it in 20, so the words are often still inside.
How do I get the text out of a corrupt DOCX file?
Work on a copy. If unzip still lists the file, use unzip -p broken.docx word/document.xml and strip the tags. If the index is gone, a short script that reads the local headers found all the text in 20 of our 28 damaged DOCX files. Microsoft also lists Recover Text from Any File in Word, which keeps text and loses formatting.
Can I repair a DOCX file with a script?
For some damage, yes. Our script cut junk bytes, undid a text-mode transfer and a UTF-8 resave, and rebuilt the ZIP index from the local headers. It also wrote back a missing content types part or relationships part. It gave all 52 test items back in 22 of 28 cases, and it cannot rebuild a flipped or zeroed document.xml.
How do I convert a DOCX file to PDF or ODT?
In Word, use Save As. With LibreOffice, use the command soffice --headless --convert-to pdf report.docx, and change pdf to odt for an ODT file. In our tests, LibreOffice and pandoc kept all 52 test items in every format we tried. The picture came out byte for byte the same in ODT, DOC and EPUB.
Can I open a DOCX file by renaming it to ZIP?
Yes, for most files. The Library of Congress says a DOCX file can be opened with ZIP software after you rename it to .zip, and our unzip command opened DOCX files with no rename. Only a ZIP-based file opens this way. A password-protected DOCX file is a compound file, and a file saved under the wrong ending is still the wrong kind.
What is the difference between DOCX, DOTX and DOCM files?
They differ by the content type of the main part. DOCX is a document, DOTX is a template, DOCM is a document with macros and DOTM is a template with macros, says the Library of Congress. All four are ZIP packages. Our templates and macro-enabled copies stopped python-docx, and the other readers we tried read them.
Is there a free DOCX viewer?
SysTools and Freeviewer both describe viewers for DOCX files. The SysTools page lists a free version for Windows and Mac, and the Freeviewer page lists a Windows demo. We did not start either one. LibreOffice is free and read all 19 of our good DOCX files that have text.
Still Stuck With a DOCX File? Eight routes out, depending on what went wrong
Start with the fixes by damage. A wiped signature, a bad index or a missing small part explains many cases, and small byte edits fix them.
Go to the password steps. You need the password, and the readers return nothing without it.
Does it end in .doc? The page on the older DOC format covers Word 97 to 2003 files.
Is it an open-source document? See the page on OpenDocument ODT files.
Do you want a fixed layout? The page on PDF documents explains the format for documents that must look the same everywhere.
Is it a ZIP archive? The page on ZIP files explains the container that every DOCX file uses.
Is it a spreadsheet or slides? The Excel XLS page and the PowerPoint PPT page cover the older formats.
Is it a file type you do not know? Browse the file extension resource library, or send it to us on the submit a file type page.