File Formats: OOXML & OLE2

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Versions and dates on this page were checked on 2026-10-09: Apache POI 5.5.1 (the 6.0 line has not been released yet and its differences are noted where they matter), Java 21 or later. The examples use the Bookshelf files described on the section landing page.

Every Excel and Word file that a server exports or receives is one of two kinds of container. Files with an x in the extension (.xlsx, .docx) are zip archives of XML documents, known as OOXML. Files from before Office 2007 (.xls, .doc) are OLE2 compound files, a small file system stored inside one file. POI has a separate code path for each, and almost every surprise in real projects (a NotOfficeXmlFileException, an unexpected memory peak, an upload that is rejected as the wrong type) comes from not knowing which container is in front of you. This page explains both containers, the POI classes that open them, the MIME types and the limits of each format. The overall component map is on Introduction & Architecture, and the Maven coordinates are on Setup & Dependencies.

Two families of formats

Extension Container Content POI classes

.xlsx

OOXML zip (OPC)

Excel workbook, SpreadsheetML

XSSFWorkbook, SXSSFWorkbook (write only), XSSFReader (events)

.xlsm

OOXML zip (OPC)

Excel workbook with a VBA project part

XSSFWorkbook reads and keeps the macro part; POI cannot create or edit macros

.xlsb

OOXML zip (OPC) holding binary parts

Excel binary workbook

Read only through the event API XSSFBReader (since 3.16; 5.5.0 added a native-cell XSSFBSheetContentsHandler callback); there is no usermodel

.xls

OLE2 compound file

Excel 97-2003 workbook, BIFF8 records

HSSFWorkbook, HSSFEventFactory (events)

.docx, .docm

OOXML zip (OPC)

Word document, WordprocessingML

XWPFDocument

.dotx

OOXML zip (OPC)

Word template

XWPFDocument opens it; its main part has a different content type

.doc

OLE2 compound file

Word 97-2003 document

HWPFDocument (jar poi-scratchpad), limited write support

The OOXML family is the standard ECMA-376, also published as ISO/IEC 29500. It has two conformance classes, Transitional and Strict, and POI targets the Transitional files that Excel and Word write by default. The binary .xls and .doc formats are documented by Microsoft in the Open Specifications (MS-XLS, MS-DOC); POI implements them record by record. Plain .csv files and the XML-only "SpreadsheetML 2003" files are not handled by POI.

A file’s extension says nothing reliable about its container: a user can rename books-import.xls to .xlsx. POI therefore detects the container from the first bytes of the file, as shown in Detecting the container.

OOXML: zip packages made of parts

An OOXML file is a zip archive organised according to the Open Packaging Conventions (OPC, part 2 of ECMA-376). OPC is independent of Word and Excel: it is a general way to store many typed pieces of content and the links between them in one file. Four ideas are enough to read any package.

Part

A named piece of content, such as /xl/worksheets/sheet1.xml or /word/media/image1.png. A part name always starts with a slash and maps directly to the zip entry name (without the slash). Parts are mostly XML, but images, fonts and VBA projects are binary.

Content type

Every part has a MIME type. The types are not stored in the part but in one special entry, [Content_Types].xml, as Default elements (by file extension, for example png) and Override elements (by exact part name). The content type is how a reader knows that /xl/workbook.xml is a workbook and not just XML.

Relationship

A typed, directed link from a source part (or from the package itself) to a target part or an external URL. Relationships are stored in a companion part named _rels/<name>.rels placed next to the source: /_rels/.rels for the package and /xl/_rels/workbook.xml.rels for /xl/workbook.xml. Each one has an Id (such as rId1), a Type URI and a Target. The XML of the source part refers to targets by Id only (the r:id attribute), never by file name, so a writer can rename a part without touching the content that uses it.

Package

The zip file as a whole: the set of parts, the package-level relationships, and the core properties (title, author, dates, stored in /docProps/core.xml).

A reader never scans the zip for known names. It reads [Content_Types].xml, follows the package relationship of type officeDocument to find the main part, and then follows that part’s relationships. This is why files written by Excel, LibreOffice, Google Sheets and POI can use different names for the same thing and still open everywhere.

The parts of an .xlsx file

The zip entries of books-inventory.xlsx on the left and the chain of relationships a reader follows from the content types and package relationships to the workbook and the first worksheet on the right
Part Content

[Content_Types].xml

Content types of all parts

_rels/.rels

Package relationships: the main workbook, core and extended properties

xl/workbook.xml

The main part: sheet names, their r:id values, defined names, calculation settings

xl/_rels/workbook.xml.rels

Maps each r:id to a worksheet, styles.xml, sharedStrings.xml or the theme

xl/worksheets/sheet1.xml

One part per sheet: rows, cells, formulas, column widths, merged regions, validations

xl/styles.xml

Fonts, fills, borders, number formats and the cell style records that cells refer to by index

xl/sharedStrings.xml

The string table: a string cell stores an index into this table

xl/drawings/, xl/charts/, xl/media/

Drawing anchors, charts and pictures

xl/tables/, xl/pivotTables/

Table definitions and pivot tables

xl/calcChain.xml

Calculation order that Excel maintains; POI removes it when it changes formulas

The mapping to the POI object model follows from this table. The cell style indexes in sheet1.xml are the reason a workbook has a limit on the number of distinct styles (see the limits table below), and the shared string table is why SXSSFWorkbook can write a very large sheet with little memory: it can write inline strings and keep no table in memory.

The parts of a .docx file

The zip entries of loan-letter.docx on the left and the relationships from the main document part to styles, header, footer, numbering and a picture on the right

A .docx has a simpler layout: the whole body of the document (paragraphs, runs, tables) is in word/document.xml. Headers, footers, styles, numbering definitions, comments, footnotes and pictures are separate parts that the main part reaches through relationships. This is why XWPF methods that add a picture or a header must create a new part and a relationship, and why copying a paragraph that contains a picture between two documents needs more than copying XML.

Main part content types

The content type of the main part tells the macro-free and macro-enabled variants of a format apart. A server that accepts uploads can read it to know what it received without opening the document model.

Format Content type of the main part

.xlsx

application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml

.xlsm

application/vnd.ms-excel.sheet.macroEnabled.main+xml

.xlsb

application/vnd.ms-excel.sheet.binary.macroEnabled.main (binary, no +xml)

.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

OpenXML4J: OPCPackage and PackagePart

OpenXML4J is the POI implementation of OPC. It lives in the package org.apache.poi.openxml4j.opc of the jar poi-ooxml. XSSFWorkbook and XWPFDocument are built on it, and you can use it directly to inspect a file without loading the whole document model, which is useful for validating uploads and for reading properties cheaply.

Class Role

OPCPackage

The package. OPCPackage.open(File, PackageAccess) opens a file; OPCPackage.open(InputStream) reads a stream. Closeable.

PackageAccess

READ, WRITE or READ_WRITE. Use READ for anything that does not save.

PackagePart

One part: getPartName(), getContentType(), getInputStream(), getRelationships().

PackagePartName

A validated part name. PackagingURIHelper.createPartName("/xl/workbook.xml") builds one.

PackageRelationship

One relationship: getId(), getRelationshipType(), getTargetURI(), getTargetMode() (INTERNAL or EXTERNAL).

PackageRelationshipTypes

Constants for the standard relationship type URIs, such as CORE_DOCUMENT and IMAGE_PART.

PackageProperties

The core properties: getTitleProperty(), getCreatorProperty(), getCreatedProperty().

The first example lists every part of books-inventory.xlsx with its content type and size, then prints the relationships of the workbook part. It opens the package read-only from a File.

import java.io.File;
import java.io.InputStream;
import java.util.List;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;

public class ListPackageParts {

    public static void main(String[] args) throws Exception {
        File file = new File(args.length > 0 ? args[0] : "books-inventory.xlsx");

        // READ: nothing is written back when the package is closed
        try (OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ)) {
            List<PackagePart> parts = pkg.getParts();
            for (PackagePart part : parts) {
                long size;
                try (InputStream in = part.getInputStream()) {
                    size = in.readAllBytes().length;
                }
                System.out.printf("%-40s %-85s %7d bytes%n",
                        part.getPartName().getName(), part.getContentType(), size);
            }

            PackagePartName workbookName = PackagingURIHelper.createPartName("/xl/workbook.xml");
            PackagePart workbook = pkg.getPart(workbookName);
            for (PackageRelationship rel : workbook.getRelationships()) {
                System.out.printf("%s %s -> %s%n", rel.getId(),
                        rel.getRelationshipType(), rel.getTargetURI());
            }
        } catch (InvalidFormatException e) {
            throw new IllegalArgumentException("Not a valid OPC package: " + file, e);
        }
    }
}

Notes on the API that matter in production:

  • OPCPackage.open(File) uses the zip central directory and reads parts on demand, so memory stays low. OPCPackage.open(InputStream) has to read every entry before returning, which makes the whole decompressed file live in memory (or in temporary files if configured). Prefer the File variant whenever the upload is already on disk.

  • With PackageAccess.READ_WRITE, close() saves the changes back to the file. To abandon changes, call revert() instead of close(). This is the most common cause of a "corrupted" file after a failed edit.

  • XSSFWorkbook and XWPFDocument accept an OPCPackage, and getPackage() returns the package behind an open document. Closing the document closes the package.

  • Opening a file that is a zip but not an OPC package throws InvalidFormatException; opening an OLE2 file throws NotOfficeXmlFileException.

  • Zip bomb limits (ZipSecureFile) are applied while parts are read. They are covered on the security pages of this section; the defaults are generous for the Bookshelf files and should be lowered for untrusted uploads.

Reading a part and its properties

Reading the main workbook part shows the sheet names without building a Workbook. This is the fastest way to answer "which sheets does this upload contain?" for a very large file.

import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackageProperties;
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;

public class ReadWorkbookPart {

    public static void main(String[] args) throws Exception {
        File file = new File("books-inventory.xlsx");
        try (OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ)) {
            PackageProperties props = pkg.getPackageProperties();
            System.out.println("Title:   " + props.getTitleProperty().orElse("(none)"));
            System.out.println("Creator: " + props.getCreatorProperty().orElse("(none)"));

            // follow the package relationship "officeDocument" to the main part
            PackagePart main = pkg.getPartsByRelationshipType(
                    PackageRelationshipTypes.CORE_DOCUMENT).get(0);
            System.out.println("Main part: " + main.getPartName() + " " + main.getContentType());

            try (InputStream in = main.getInputStream()) {
                String xml = new String(in.readAllBytes(), StandardCharsets.UTF_8);
                System.out.println(xml.substring(0, Math.min(400, xml.length())));
            }
        }
    }
}

Parsing XML by hand like this is only for inspection. To read cell data from a large file use the event API described in the Excel pages of this section, which is built on the same classes (XSSFReader opens the package and walks these relationships for you).

OLE2: a file system inside a file

An .xls or .doc file is an OLE2 compound document (also called Compound File Binary, CFB; format defined in MS-CFB). It is a small file system with its own sectors, a file allocation table, directories and files, all stored in one file. The file starts with the eight-byte signature D0 CF 11 E0 A1 B1 1A E1. The sector size is 512 bytes in the usual version 3 files, and 4096 bytes in version 4 files. Data smaller than 4096 bytes is stored in a separate "mini stream" made of 64-byte sectors, which is why many tiny streams do not waste space.

In OLE2 terminology a directory is a storage and a file is a stream. The application data lives in well-known streams:

Stream Content

Workbook (or Book in BIFF5 files)

The Excel BIFF record stream: every cell, style and formula of an .xls

WordDocument

The main Word binary stream; 0Table or 1Table holds the tables that describe it, and Data holds embedded data

\005SummaryInformation, \005DocumentSummaryInformation

Document properties in the HPSF property set format (title, author, word count)

EncryptionInfo and EncryptedPackage

Present in an OLE2 file that wraps an encrypted .xlsx or .docx; see the encryption page of this section

Ole, ObjectPool, _VBA_PROJECT_CUR

Embedded objects and VBA macros

Note the last two rows of the first group: an encrypted .xlsx is an OLE2 file, not a zip. This is the reason a password-protected books-import.xlsx fails to open as OOXML and must be opened through POIFS first.

POIFS classes

POIFS ("Poor Obfuscation Implementation File System", a joke on the original name of the OLE2 format) is the POI implementation of OLE2. It lives in org.apache.poi.poifs.filesystem of the jar poi.

Class Role

POIFSFileSystem

The whole compound file. Constructors take a File (optionally read-only), an InputStream, or nothing (new empty file).

DirectoryEntry and DirectoryNode

A storage. getRoot() returns the root; getEntryNames(), getEntry(name) and hasEntry(name) list and find children.

DocumentEntry

A stream: getName() and getSize().

DocumentInputStream

Reads a stream: fs.createDocumentInputStream("Workbook").

FileMagic

Identifies the container from the first bytes: OLE2, OOXML, XML, PDF, and others.

HSSFWorkbook and HWPFDocument are built on POIFS in the same way as XSSFWorkbook is built on OPC: new HSSFWorkbook(fs) reads the Workbook stream of the filesystem you give it.

The second example lists the entries of an old .xls export and opens its workbook stream.

import java.io.File;
import java.util.Iterator;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

public class ListOle2Entries {

    public static void main(String[] args) throws Exception {
        File file = new File("books-inventory-legacy.xls");

        // File + read-only: the file is accessed through a channel, not copied to the heap
        try (POIFSFileSystem fs = new POIFSFileSystem(file, true)) {
            DirectoryEntry root = fs.getRoot();
            for (Iterator<Entry> it = root.getEntries(); it.hasNext(); ) {
                Entry entry = it.next();
                if (entry instanceof DocumentEntry doc) {
                    System.out.printf("stream  %-40s %8d bytes%n", doc.getName(), doc.getSize());
                } else {
                    System.out.printf("storage %s%n", entry.getName());
                }
            }

            System.out.println("Has Workbook stream: " + root.hasEntry("Workbook"));

            // a workbook can be built from the already open filesystem
            try (HSSFWorkbook workbook = new HSSFWorkbook(fs)) {
                System.out.println("Sheets: " + workbook.getNumberOfSheets());
            }
        }
    }
}

File versus InputStream

The choice between the two constructors decides the memory cost of reading an OLE2 file:

Constructor Memory Notes

new POIFSFileSystem(File) or new POIFSFileSystem(File, true)

Backed by the file; only the parts that are accessed are read

Pass true for read-only: the file is not locked for writing and a modification is rejected. A writable instance writes back with writeFilesystem().

new POIFSFileSystem(InputStream)

Reads the whole stream into heap memory

Unavoidable for uploads that are only available as a stream. Copy the upload to a temporary file first when files are large.

POIFS also enforces record size limits through IOUtils.setByteArrayMaxOverride, to stop a malicious .xls or .doc from asking for a gigabyte-sized record; these limits are covered on the security pages. HSSF and HWPF read the whole document into an object model, so even with a file-backed POIFS the memory used is a multiple of the file size.

Detecting the container

A server rarely knows whether an upload is .xls or .xlsx. FileMagic looks at the first bytes, and WorkbookFactory uses the same check to return an HSSFWorkbook or an XSSFWorkbook behind the common Workbook interface. The example below does both and rejects anything else, instead of trusting the file name or the Content-Type header sent by the browser.

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

public class DetectAndOpen {

    public static void main(String[] args) throws Exception {
        File upload = new File("books-import.xlsx");

        try (InputStream raw = new BufferedInputStream(new FileInputStream(upload))) {
            // peeks at the first bytes and resets the stream
            FileMagic magic = FileMagic.valueOf(FileMagic.prepareToCheckMagic(raw));
            switch (magic) {
                case OLE2 -> System.out.println("OLE2: legacy .xls (or an encrypted OOXML file)");
                case OOXML -> System.out.println("OOXML: zip package");
                default -> throw new IllegalArgumentException("Not an Excel file: " + magic);
            }
        }

        // read-only, from a File: avoids copying the whole file to memory
        try (Workbook workbook = WorkbookFactory.create(upload, null, true)) {
            System.out.println(workbook.getClass().getSimpleName()
                    + " with " + workbook.getNumberOfSheets() + " sheet(s)");
        }
    }
}

FileMagic.OLE2 for a file that is supposed to be .xlsx has two possible causes: a renamed legacy file or a password-protected file. Telling them apart means checking for the EncryptedPackage entry in the POIFS root, as the encryption page of this section shows. Detecting the container proves only that the bytes have the right shape; it does not make an untrusted file safe, see the security pages.

MIME types

The Content-Type of a download response and the Content-Type the browser sends with an upload must use the registered types. They are listed in the IANA media types registry.

Format MIME type

.xlsx

application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

.xlsm

application/vnd.ms-excel.sheet.macroEnabled.12

.xlsb

application/vnd.ms-excel.sheet.binary.macroEnabled.12

.xls

application/vnd.ms-excel

.docx

application/vnd.openxmlformats-officedocument.wordprocessingml.document

.dotx

application/vnd.openxmlformats-officedocument.wordprocessingml.template

.docm

application/vnd.ms-word.document.macroEnabled.12

.doc

application/msword

Two practical rules follow. When sending a file, set the exact type from this table together with a Content-Disposition: attachment header, otherwise browsers may display the zip bytes or save a file named .zip. When receiving a file, treat the declared type and the extension as hints: the OOXML types are long and easy to mistype in an allow list, a browser may send application/octet-stream, and a client controls the value. The authoritative check is the content check shown above.

Format limits

The limits come from the file formats, not from POI, and they differ between the two families. POI exposes them in the enum org.apache.poi.ss.SpreadsheetVersion so that code can validate data before writing and fail with a useful message instead of a corrupt file.

Limit .xls (EXCEL97) .xlsx (EXCEL2007)

Rows per sheet

65,536

1,048,576

Columns per sheet

256 (A to IV)

16,384 (A to XFD)

Characters in a cell

32,767

32,767

Distinct cell styles in a workbook

4,000

64,000

Characters in a sheet name

31

31

The Word formats have no fixed page or paragraph limit, but they share the practical limits of memory: both HWPF and XWPF build the full document in memory.

The text length limit is the one that bites exports: a long description from the Bookshelf catalogue that is written to a cell is rejected by POI with an IllegalArgumentException when it exceeds the maximum, rather than being truncated. The last example validates the data first and also shows why a reader that handles both families should compare against the limits of the workbook it actually opened.

import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

public class CheckLimits {

    public static void main(String[] args) throws Exception {
        for (SpreadsheetVersion version : SpreadsheetVersion.values()) {
            if (version == SpreadsheetVersion.EXCEL97 || version == SpreadsheetVersion.EXCEL2007) {
                System.out.printf("%s: %d rows, %d columns, %d characters per cell, %d cell styles%n",
                        version, version.getMaxRows(), version.getMaxColumns(),
                        version.getMaxTextLength(), version.getMaxCellStyles());
            }
        }

        try (Workbook workbook = new XSSFWorkbook()) {
            SpreadsheetVersion limits = workbook.getSpreadsheetVersion();
            Sheet sheet = workbook.createSheet("Books");

            String description = "A very long description. ".repeat(2000);
            if (description.length() > limits.getMaxTextLength()) {
                // keep the export valid: truncate and tell the user, do not let POI throw
                description = description.substring(0, limits.getMaxTextLength() - 1) + "…";
            }
            Row row = sheet.createRow(0);
            row.createCell(0).setCellValue(description);
            System.out.println("Cell length: " + description.length());
        }
    }
}

Choosing between the families

  • New files: always create OOXML. Writing .xls is only justified when a downstream system cannot read .xlsx, and then the 65,536 row limit and the 4,000 style limit must be checked before writing.

  • Reading uploads: accept .xlsx and .docx by default; accept .xls and .doc only if users really have such files, because the OLE2 parsers are older, less complete and historically the source of many POI security fixes.

  • .xlsb can only be read through the event API; ask users for .xlsx if you need random access.

  • Macro-enabled files (.xlsm, .docm) keep their macros when POI loads and saves them, but POI never executes them. An allow list that accepts .xlsx only should check the main part content type, since the extension can be renamed.

  • The 6.0 line is announced to remove deprecated APIs and to harden the readers against zip bombs and path traversal; the container classes described here are not expected to change shape, but check the POI release notes when it ships.