Word Tables, Lists & Images
|
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. |
Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; the 6.0 line
is in development and has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11.
It covers the three building blocks that turn a plain Word document into a report: tables, lists and pictures. It
assumes the paragraph and run model from Word XWPF Basics; headers,
footers, sections and fields are in
Word Headers, Footers, Sections & Fields, and
replacing text in an existing document is in
Word Templates & Placeholder Replacement.
The examples use the Bookshelf scenario described in the section landing page:
the monthly report (monthly-report.docx) lists the loans of a month in a table, describes the rules in numbered and
bulleted lists and shows the library logo and a cover picture. The package structure behind these objects is in
File Formats: OOXML & OLE2.
XWPF is the part of POI that is closest to the raw XML. Tables, numbering and pictures expose only a thin object model,
and for anything beyond the basics (merged cells, multi-level numbering definitions, floating pictures) you reach for
the generated XMLBeans classes in org.openxmlformats.schemas.wordprocessingml.x2006.main (the CT... classes). The
default poi-ooxml-lite jar holds the beans that POI itself uses, which include every one used on this page (CTTcPr,
CTTcBorders, STMerge, CTAbstractNum, CTLvl and the rest); poi-ooxml-full is needed only for beans outside that
set. See Setup & Dependencies for the coordinates and for the
NoClassDefFoundError that appears when a bean is missing from the lite jar.
The table model
A Word table is a w:tbl element in the body (or in a table cell, because tables can nest). It contains a table
properties element (w:tblPr), a grid of column definitions (w:tblGrid) and rows (w:tr); each row contains cells
(w:tc), and each cell contains one or more block elements, normally paragraphs and sometimes another table. A cell
must always end with a paragraph, otherwise Word reports the file as corrupt.
body elements"] --> T["XWPFTable
w:tbl, w:tblPr, w:tblGrid"] T --> R1["XWPFTableRow
w:tr, w:trPr"] T --> R2["XWPFTableRow"] R1 --> C1["XWPFTableCell
w:tc, w:tcPr"] R1 --> C2["XWPFTableCell"] C1 --> P1["XWPFParagraph
w:p"] C1 --> N["XWPFTable
nested table"] P1 --> RUN["XWPFRun
w:r, text or picture"]
The mapping between the Java classes and the XML is:
| Class | XML element | Notes |
|---|---|---|
|
|
Created with |
|
|
Height, header-row repeat ( |
|
|
Width, shading, vertical alignment and, through |
|
|
The cell text. |
The Javadoc for the whole package is at https://poi.apache.org/apidocs/dev/org/apache/poi/xwpf/usermodel/package-summary.html.
Creating a table
XWPFDocument.createTable(rows, cols) appends a table with the given number of rows and columns to the end of the body;
each cell already contains one empty paragraph. createTable() without arguments creates one row with one cell, to
which you add cells with XWPFTableRow.addNewTableCell() and rows with XWPFTable.createRow(). A new row created with
createRow() copies the number of cells of the first row.
The first example writes the loans table of the monthly report. It also shows the pattern used on the rest of the page: one small helper that writes a cell with a formatted run, so that the table code stays short.
package com.example.bookshelf.report;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
public final class LoansTableReport {
public record LoanLine(String title, String member, LocalDate loanedOn, LocalDate dueOn, int renewals) { }
private static final DateTimeFormatter DATE = DateTimeFormatter.ISO_LOCAL_DATE;
private LoansTableReport() { }
public static void write(List<LoanLine> loans, Path target) throws IOException {
try (XWPFDocument document = new XWPFDocument();
OutputStream out = Files.newOutputStream(target)) {
XWPFParagraph heading = document.createParagraph();
XWPFRun title = heading.createRun();
title.setBold(true);
title.setFontSize(16);
title.setText("Loans of the month");
// one header row plus one row per loan, five columns
XWPFTable table = document.createTable(loans.size() + 1, 5);
String[] headers = {"Title", "Member", "Loaned on", "Due on", "Renewals"};
XWPFTableRow headerRow = table.getRow(0);
for (int c = 0; c < headers.length; c++) {
writeCell(headerRow.getCell(c), headers[c], true, ParagraphAlignment.LEFT);
}
for (int r = 0; r < loans.size(); r++) {
LoanLine loan = loans.get(r);
XWPFTableRow row = table.getRow(r + 1);
writeCell(row.getCell(0), loan.title(), false, ParagraphAlignment.LEFT);
writeCell(row.getCell(1), loan.member(), false, ParagraphAlignment.LEFT);
writeCell(row.getCell(2), DATE.format(loan.loanedOn()), false, ParagraphAlignment.LEFT);
writeCell(row.getCell(3), DATE.format(loan.dueOn()), false, ParagraphAlignment.LEFT);
writeCell(row.getCell(4), Integer.toString(loan.renewals()), false, ParagraphAlignment.RIGHT);
}
document.write(out);
}
}
/** Writes text into the (single, empty) paragraph that every new cell already contains. */
static void writeCell(XWPFTableCell cell, String text, boolean bold, ParagraphAlignment alignment) {
XWPFParagraph paragraph = cell.getParagraphs().get(0);
paragraph.setAlignment(alignment);
XWPFRun run = paragraph.createRun();
run.setBold(bold);
run.setText(text);
}
}
The helper writes to cell.getParagraphs().get(0) instead of calling cell.addParagraph(). A cell created by POI
already has one paragraph, and adding another leaves an empty line at the top of every cell. cell.setText(String) is a
shortcut that does the same job when the text needs no formatting.
The table is created with document.createTable(rows, cols) at the current end of the body. Elements are appended in the
order they are created, so the heading paragraph above comes first. To insert a table at a specific position, for
example at a placeholder paragraph in a template, you need the cursor-based XWPFDocument.insertNewTbl(XmlCursor);
that technique is shown in
Word Templates & Placeholder Replacement.
Widths, layout and alignment
Word stores widths in twips (a twentieth of a point, so 1440 twips are one inch), or as percentages in fiftieths of a
percent (5000 is 100 percent), or as "auto". POI’s XWPFTable.setWidth(String) and XWPFTableCell.setWidth(String)
accept either form:
| Value passed | Resulting w:type |
Meaning |
|---|---|---|
|
|
Percentage of the available width. POI converts the percentage to fiftieths of a percent. |
|
|
Absolute width in twips (4320 twips are three inches, about 7.6 cm). |
|
|
Width computed by Word from the content. |
A4 portrait with the default Word margins of 2.54 cm leaves about 9026 twips for content, and US Letter leaves about 9360 twips. Percentages avoid hard-coding that number. When you mix absolute cell widths, make them add up to the table width, otherwise Word and LibreOffice resolve the conflict in different ways.
The second example sets widths for the five loan columns, centres the table and adds cell margins. The proportions are the Bookshelf choice: a wide title column and narrow numeric columns.
package com.example.bookshelf.report;
import org.apache.poi.xwpf.usermodel.TableRowAlign;
import org.apache.poi.xwpf.usermodel.TableWidthType;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
public final class TableLayout {
/** Column widths as percentages of the table width; they must add up to 100. */
private static final int[] PERCENT = {32, 24, 16, 16, 12};
private TableLayout() { }
public static void apply(XWPFTable table) {
table.setWidth("100%");
table.setTableAlignment(TableRowAlign.CENTER);
// top, left, bottom, right in twips: 60 twips are about 1 mm
table.setCellMargins(60, 100, 60, 100);
for (XWPFTableRow row : table.getRows()) {
for (int c = 0; c < PERCENT.length; c++) {
row.getCell(c).setWidth(PERCENT[c] + "%");
}
}
}
public static void autoFit(XWPFTable table) {
table.setWidthType(TableWidthType.AUTO);
}
}
POI writes the width into every cell (w:tcW) but does not rebuild the w:tblGrid element that holds the column
definitions. Word honours the cell widths. Other consumers, notably LibreOffice and some PDF converters, prefer the
grid, so a table can look right in Word and slightly different elsewhere. If the exact column proportions matter to a
non-Word consumer, set the CTTblGrid column widths through table.getCTTbl().getTblGrid() as well, or create the
table in a template where the grid is already correct.
XWPFTable.setWidth(int) also exists but takes the width in twips with the type set to dxa; the string form is
clearer because the unit is visible in the value.
Borders, shading and alignment inside cells
Table-wide borders are set with six methods on XWPFTable: setTopBorder, setBottomBorder, setLeftBorder,
setRightBorder, setInsideHBorder (the horizontal lines between rows) and setInsideVBorder (the vertical lines
between columns). Each takes the same four arguments:
| Argument | Meaning |
|---|---|
|
The line style: |
|
The line width in eighths of a point. The value 4 is half a point, 8 is one point. Word accepts 2 to 96. |
|
The distance between the border and the text, in points. Zero is the usual value for tables. |
|
Six hexadecimal digits without |
Cell content is aligned in two directions. Horizontal alignment belongs to the paragraph
(XWPFParagraph.setAlignment(ParagraphAlignment), used by the helper above). Vertical alignment belongs to the cell
(XWPFTableCell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign) with TOP, CENTER, BOTTOM or BOTH). Shading is
XWPFTableCell.setColor(String), which writes a solid background fill and takes the same six-digit hex colour. POI has
no pattern fill, gradient or theme-colour API for cells; for those you edit getCTTc().getTcPr() directly.
The third example styles the loans table: a light grid, a shaded header, zebra rows for readability and a highlighted overdue row.
package com.example.bookshelf.report;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTable.XWPFBorderType;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
public final class TableStyling {
private static final String GRID = "BFBFBF";
private static final String HEADER_FILL = "1F3864";
private static final String ZEBRA_FILL = "F2F2F2";
private static final String OVERDUE_FILL = "FCE4D6";
private TableStyling() { }
public static void apply(XWPFTable table, java.util.Set<Integer> overdueRows) {
table.setTopBorder(XWPFBorderType.SINGLE, 8, 0, "000000");
table.setBottomBorder(XWPFBorderType.SINGLE, 8, 0, "000000");
table.setLeftBorder(XWPFBorderType.NONE, 0, 0, "auto");
table.setRightBorder(XWPFBorderType.NONE, 0, 0, "auto");
table.setInsideHBorder(XWPFBorderType.SINGLE, 4, 0, GRID);
table.setInsideVBorder(XWPFBorderType.NONE, 0, 0, "auto");
for (int r = 0; r < table.getNumberOfRows(); r++) {
XWPFTableRow row = table.getRow(r);
String fill = null;
if (r == 0) {
fill = HEADER_FILL;
} else if (overdueRows.contains(r)) {
fill = OVERDUE_FILL;
} else if (r % 2 == 0) {
fill = ZEBRA_FILL;
}
for (XWPFTableCell cell : row.getTableCells()) {
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
if (fill != null) {
cell.setColor(fill);
}
}
}
// white text on the dark header fill
for (XWPFTableCell cell : table.getRow(0).getTableCells()) {
cell.getParagraphs().get(0).getRuns().forEach(run -> run.setColor("FFFFFF"));
}
}
}
Borders on a single cell have no method on XWPFTableCell in 5.5.1. The usual approach is to write the element
yourself:
import java.math.BigInteger;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcBorders;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
static void bottomBorder(XWPFTableCell cell, String rgb) {
CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
CTTcBorders borders = properties.isSetTcBorders() ? properties.getTcBorders() : properties.addNewTcBorders();
CTBorder bottom = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom();
bottom.setVal(STBorder.SINGLE);
bottom.setSz(BigInteger.valueOf(12));
bottom.setSpace(BigInteger.ZERO);
bottom.setColor(rgb);
}
The isSet... and addNew... pair is the standard XMLBeans idiom: it reuses an existing child element and creates it
only when it is missing, so the code works for tables read from a template as well as for new ones.
Row options: repeated headers, heights and page breaks
Three settings on XWPFTableRow matter for a table that spans pages:
| Method | Effect |
|---|---|
|
Marks the row as a header row ( |
|
Prevents a row from breaking across two pages ( |
|
Row height in twips. |
XWPFTableRow header = table.getRow(0);
header.setRepeatHeader(true);
header.setHeight(420);
header.setHeightRule(TableRowHeightRule.AT_LEAST);
for (int r = 1; r < table.getNumberOfRows(); r++) {
table.getRow(r).setCantSplitRow(true);
}
Adding and removing rows works on the existing table. table.createRow() appends a row with the same number of cells as
the first row, table.insertNewTableRow(int) inserts one at a position, table.addRow(XWPFTableRow, int) places a row
you built yourself and table.removeRow(int) deletes one. For a variable number of rows, build the table with one
header row and call createRow() once per record, so you do not need to know the count in advance:
XWPFTable table = document.createTable(1, 3);
// fill row 0 with the header, then
for (Book book : books) {
XWPFTableRow row = table.createRow();
row.getCell(0).setText(book.title());
row.getCell(1).setText(book.author());
row.getCell(2).setText(Integer.toString(book.copies()));
}
Merged cells
Word has two kinds of merge, and neither has a method in the XWPF object model; both are written on the cell properties
(w:tcPr):
-
A horizontal merge makes one cell span several grid columns:
w:gridSpanwith the number of columns. The cells that are covered must be removed from the row, because a row with a spanning cell has fewerw:tcelements than the grid has columns. -
A vertical merge uses
w:vMerge. The first cell of the merged range hasvMergewith the valuerestart; every cell below it that belongs to the merge has an emptyvMerge(the valuecontinueis the default). The cells stay in their rows, so the row structure is unchanged.
The example builds a summary block for the monthly report: a title cell spanning all four columns, and a "Section" column that groups the rows below it vertically.
package com.example.bookshelf.report;
import java.math.BigInteger;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
public final class TableMerges {
private TableMerges() { }
/** Merges columns first..last (inclusive, zero based) of one row into the cell at index first. */
public static void mergeHorizontally(XWPFTable table, int rowIndex, int first, int last) {
XWPFTableRow row = table.getRow(rowIndex);
XWPFTableCell cell = row.getCell(first);
CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
properties.addNewGridSpan().setVal(BigInteger.valueOf(last - first + 1L));
// remove the covered cells from the end so the indexes stay valid
for (int c = last; c > first; c--) {
row.removeCell(c);
}
}
/** Merges rows first..last (inclusive, zero based) of one column into the cell at row first. */
public static void mergeVertically(XWPFTable table, int column, int first, int last) {
for (int r = first; r <= last; r++) {
XWPFTableCell cell = table.getRow(r).getCell(column);
CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
if (r == first) {
properties.addNewVMerge().setVal(STMerge.RESTART);
} else {
properties.addNewVMerge().setVal(STMerge.CONTINUE);
}
}
}
}
A typical call for the summary block is mergeHorizontally(table, 0, 0, 3) for the title row and
mergeVertically(table, 0, 1, 3) for the group column. Two points are easy to get wrong:
-
Merge after you have written all the cell text and widths of the row. After
removeCell, the row has fewer cells and the column indexes no longer match the grid; do not callrow.getCell(c)with an old index afterwards. -
A merge does not move text. If the covered cells contained text, it is lost with the removed cells (horizontal) or hidden by Word (vertical), so write the text into the first cell only.
An old pattern found in older tutorials calls cell.getCTTc().addNewTcPr().addNewVMerge() twice for the same cell, which
creates an invalid duplicate element. Reuse the existing tcPr as shown above.
Table styles
XWPFTable.setStyleID(String) assigns a style from the document’s styles.xml, such as the built-in Word grid style with
the id TableGrid. The method only writes the id. A document created with new XWPFDocument() has no style part at
all (getStyles() returns null), so TableGrid is undefined, the call has no visible effect and Word shows an
unstyled table with no borders.
There are two reliable ways to use styles:
-
Start from a document that already defines the styles (a template, covered in Word Templates & Placeholder Replacement) and assign the style id that exists in it.
document.getStyles().styleExist("TableGrid")tells you whether it does. -
Format the table directly, as the examples above do. Direct formatting is verbose but does not depend on the document.
POI can create a style through XWPFStyles.addStyle(XWPFStyle) with a CTStyle you build, but there is no high-level
builder for table styles (conditional formatting for first row, banded rows and so on). For a corporate look, a template
with real styles is simpler and also makes the report editable in Word by non-programmers.
Reading tables
Tables are body elements, so reading them mirrors writing. XWPFDocument.getTables() returns the tables of the main
body in order; headers, footers, footnotes and table cells have their own getTables(). The traversal is therefore
recursive when nested tables must be reached, and the text of a nested table is not part of XWPFTableCell.getText()
of the outer cell. getTextRecursively() includes it.
The following example reads the first column of every table into a list of values, descending into nested tables. It is
the shape of the extraction used for book-request-form.docx in the import section.
package com.example.bookshelf.reader;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
public final class TableReader {
private TableReader() { }
public static List<List<String>> readAll(Path file) throws IOException {
List<List<String>> rows = new ArrayList<>();
try (XWPFDocument document = new XWPFDocument(java.nio.file.Files.newInputStream(file))) {
for (XWPFTable table : document.getTables()) {
collect(table, rows);
}
}
return rows;
}
private static void collect(XWPFTable table, List<List<String>> rows) {
for (XWPFTableRow row : table.getRows()) {
List<String> values = new ArrayList<>();
for (XWPFTableCell cell : row.getTableCells()) {
values.add(cell.getText().strip());
for (XWPFTable nested : cell.getTables()) {
collect(nested, rows);
}
}
rows.add(values);
}
}
}
The XWPFDocument(InputStream) constructor is used here because the example is stream oriented; when you have a file,
OPCPackage.open(File) followed by new XWPFDocument(pkg) avoids copying the whole stream into memory, as explained in
File Formats: OOXML & OLE2. Three behaviours to remember
when reading:
-
Merged cells appear differently: a horizontally merged cell is one entry in
getTableCells()and the row looks shorter than the header; a vertically merged range is several cells of which only the first has text. -
XWPFTableRow.getTableCells()returns onlyw:tccells. Cells wrapped in a structured document tag (a content control) inside the row are not part of that list in 5.5.1; the form-extraction page covers content controls. -
Whitespace is preserved. Use
strip()for comparison, as Word often adds trailing spaces.
Lists and numbering
Word does not store a list as a container of items. A list item is an ordinary paragraph whose paragraph properties point
to a numbering instance: w:numPr with a w:numId (which instance) and a w:ilvl (which indentation level). The
numbering part (word/numbering.xml) holds two layers of definitions:
-
an abstract numbering (
w:abstractNum) defines the appearance of up to nine levels: the number format (decimal, lower letter, bullet), the text pattern (%1.or%1.%2), the start value and the indentation of each level; -
a numbering instance (
w:num) points to an abstract numbering and can override a level, typically to restart the numbering at 1.
Paragraphs refer to instances, instances to abstract definitions. Two paragraphs with the same numId belong to the
same list and continue its counter; paragraphs with a different numId start a new list even when both instances use the
same abstract definition.
setNumID(2), setNumILvl(0)"] --> N2["w:num 2"] P2["XWPFParagraph
setNumID(2), setNumILvl(1)"] --> N2 P3["XWPFParagraph
setNumID(3), setNumILvl(0)"] --> N3["w:num 3
lvlOverride startOverride 1"] N2 --> A1["w:abstractNum 1
decimal, 9 levels"] N3 --> A1 P4["XWPFParagraph
setNumID(4), setNumILvl(0)"] --> N4["w:num 4"] N4 --> A2["w:abstractNum 2
bullet"]
In POI the numbering part is XWPFNumbering, reached with document.createNumbering() (it returns the existing part or
creates an empty one) or document.getNumbering() (which may return null when the document has none). The calls you
need are:
| Call | Purpose |
|---|---|
|
Wraps an abstract numbering that you build with the XMLBeans |
|
Adds it and returns its id (the |
|
Creates a numbering instance for that abstract numbering and returns the new |
|
Gives access to the |
|
Attach a paragraph to the list instance and set its level (zero based). |
The following class is a small reusable list factory for the monthly report: a bullet definition with three levels, a
decimal definition with 1., 1.1 and a) levels, and a method that creates a fresh list from either definition.
package com.example.bookshelf.report;
import java.math.BigInteger;
import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFNumbering;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat;
public final class ListFactory {
private final XWPFNumbering numbering;
private final BigInteger bulletDefinition;
private final BigInteger decimalDefinition;
public ListFactory(XWPFDocument document) {
this.numbering = document.createNumbering();
this.bulletDefinition = define(1, STNumberFormat.BULLET, new String[] {"•", "–", "▪"});
this.decimalDefinition = define(2, STNumberFormat.DECIMAL, new String[] {"%1.", "%1.%2", "%3)"});
}
private BigInteger define(int abstractId, STNumberFormat.Enum format, String[] levelText) {
CTAbstractNum definition = CTAbstractNum.Factory.newInstance();
definition.setAbstractNumId(BigInteger.valueOf(abstractId));
for (int level = 0; level < levelText.length; level++) {
CTLvl lvl = definition.addNewLvl();
lvl.setIlvl(BigInteger.valueOf(level));
lvl.addNewStart().setVal(BigInteger.ONE);
// the third level of the decimal list uses letters: a), b), c)
boolean lastOfDecimal = format == STNumberFormat.DECIMAL && level == 2;
lvl.addNewNumFmt().setVal(lastOfDecimal ? STNumberFormat.LOWER_LETTER : format);
lvl.addNewLvlText().setVal(levelText[level]);
// 720 twips of indent per level, with a 360 twips hanging indent for the marker
lvl.addNewPPr().addNewInd().setLeft(BigInteger.valueOf(720L * (level + 1)));
lvl.getPPr().getInd().setHanging(BigInteger.valueOf(360));
}
XWPFAbstractNum abstractNum = new XWPFAbstractNum(definition);
return numbering.addAbstractNum(abstractNum);
}
/** Starts a new bulleted list and returns its instance id. */
public BigInteger newBulletList() {
return numbering.addNum(bulletDefinition);
}
/** Starts a new numbered list whose counter starts at 1, and returns its instance id. */
public BigInteger newNumberedList() {
BigInteger numId = numbering.addNum(decimalDefinition);
return numId;
}
public static void item(XWPFParagraph paragraph, BigInteger numId, int level, String text) {
paragraph.setNumID(numId);
paragraph.setNumILvl(BigInteger.valueOf(level));
paragraph.createRun().setText(text);
}
}
Used from the report, the rules of the library become a list with a nested level:
ListFactory lists = new ListFactory(document);
BigInteger rules = lists.newNumberedList();
ListFactory.item(document.createParagraph(), rules, 0, "Loans last 21 days.");
ListFactory.item(document.createParagraph(), rules, 1, "Reference books cannot be borrowed.");
ListFactory.item(document.createParagraph(), rules, 0, "A loan can be renewed twice.");
BigInteger notes = lists.newBulletList();
ListFactory.item(document.createParagraph(), notes, 0, "Overdue books are listed in red.");
The numbers are not stored in the file. Word computes them when it renders the paragraphs, so the result of the first
list is "1.", "1.1" and "2.", with no numbers in the XML. That has one consequence for reading: paragraph.getNumFmt() and
paragraph.getNumLevelText() describe the format, but the displayed counter value is not available from POI.
Restarting a list
Using the same numId for a second list continues the first list’s counter. A second list that must start at 1 needs
either a new w:num (as newNumberedList() does: each call produces a new numId) or an explicit start override on a
shared instance:
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl;
BigInteger restarted = numbering.addNum(decimalDefinition);
CTNumLvl override = numbering.getNum(restarted).getCTNum().addNewLvlOverride();
override.setIlvl(BigInteger.ZERO);
override.addNewStartOverride().setVal(BigInteger.valueOf(5)); // this list starts at 5
Lists in templates and in existing documents
When you open a document that already contains lists, the numbering part contains the definitions the author created.
You can reuse them by taking the numId from an existing list paragraph (paragraph.getNumID()) and applying it to new
paragraphs; every item then looks exactly like the template. Listing the instances with numbering.getNums() helps to
find the right one. This is more robust than building definitions in code when the document has a designed look, and it
is the recommended approach for the report templates of the next pages.
POI does not give access to the list styles that Word offers in the ribbon (for example "List Bullet" or "List
Number"). They are paragraph styles that point to a numbering instance, and they exist only if the document defines them:
paragraph.setStyle("ListBullet") writes the style id and nothing else, and has no effect when the style is missing.
Pictures
A picture in a Word document has three layers, and POI hides the first two:
-
The image file is stored as a part under
word/media/(see the package diagram in File Formats: OOXML & OLE2) and referenced by a relationship id. -
A drawing element in a run (
w:drawing) places the picture: inline (wp:inline, flowing with the text like a character) or anchored (wp:anchor, floating with text wrapping). -
The run belongs to a paragraph, which can be in the body, in a table cell, in a header or footer.
XWPFRun.addPicture(InputStream, PictureType, String filename, int width, int height) stores the data, creates the
inline drawing and returns the XWPFPicture. It throws InvalidFormatException and IOException, and the stream is
read to the end but not closed, so open it in a try-with-resources block. The width and height are in EMU (English
Metric Units): 914400 EMU are one inch, 360000 are one centimetre, 12700 are one point and 9525 are one pixel at 96 dpi.
org.apache.poi.util.Units converts them: Units.toEMU(double points), Units.pixelToEMU(int) and the constants
Units.EMU_PER_CENTIMETER, Units.EMU_PER_POINT and Units.EMU_PER_PIXEL. Passing pixels directly produces a
picture 1/9525 of the intended size (1/12700 for points), a speck on the page; this is the most common mistake with
the API.
The PictureType enumeration (org.apache.poi.common.usermodel.PictureType) names the format, for example PNG, JPEG,
GIF, BMP, TIFF, EMF, WMF and SVG. The older overload that takes an int constant from XWPFDocument
(PICTURE_TYPE_PNG and so on) still exists and delegates to the enum version; new code should use the enum.
| Format | PictureType |
Notes |
|---|---|---|
PNG |
|
Best for logos and charts rendered by the server. Lossless. |
JPEG |
|
Photographs. Do not use |
GIF, BMP, TIFF |
|
Accepted. BMP and TIFF are large; convert them to PNG first. |
EMF, WMF |
|
Vector formats from Windows applications. Rendered by Word and LibreOffice. |
SVG |
|
Stored as a part, but Word 2016 and earlier show nothing without a PNG fallback. Prefer PNG. |
Adding a picture with its natural proportions
The size you pass is the display size; POI does not read the image dimensions for you. A size that does not match the
image’s aspect ratio stretches it. The fourth example reads the dimensions with ImageIO, scales the picture to a given
maximum width in centimetres, keeps the aspect ratio and sets the alternative text for accessibility.
package com.example.bookshelf.report;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.imageio.ImageIO;
import org.apache.poi.common.usermodel.PictureType;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFRun;
public final class Pictures {
private Pictures() { }
/** Adds a picture of at most maxWidthCm centimetres wide, with the original aspect ratio. */
public static XWPFPicture addScaled(XWPFParagraph paragraph, Path image, PictureType type,
double maxWidthCm, String altText)
throws IOException, InvalidFormatException {
BufferedImage dimensions = ImageIO.read(image.toFile());
if (dimensions == null) {
throw new IOException("Not a readable image: " + image.getFileName());
}
double ratio = (double) dimensions.getHeight() / dimensions.getWidth();
int widthEmu = (int) Math.round(maxWidthCm * Units.EMU_PER_CENTIMETER);
int heightEmu = (int) Math.round(widthEmu * ratio);
paragraph.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = paragraph.createRun();
try (InputStream data = Files.newInputStream(image)) {
XWPFPicture picture = run.addPicture(data, type, image.getFileName().toString(), widthEmu, heightEmu);
// alternative text is read by screen readers; it lives on the drawing's docPr element
run.getCTR().getDrawingArray(0).getInlineArray(0).getDocPr().setDescr(altText);
return picture;
}
}
}
ImageIO reads PNG, JPEG, GIF and BMP. For other formats, or to avoid decoding a large image only to learn its size,
read the dimensions from a metadata reader or store them with the asset. The method returns the XWPFPicture, whose
getCTPicture() exposes the underlying element for cropping or rotation.
The file name passed to addPicture becomes the picture’s name in the document (pic:cNvPr) and appears in the Word
selection pane; it is not used to load anything. The XWPFDocument de-duplicates identical image data by checksum, so
adding the same logo to every section stores it once in word/media/.
Pictures in table cells and side by side
A picture goes wherever a run goes, so a picture in a table cell is the same call on the cell’s paragraph. A two-column table with no borders is the standard way to put two pictures or a picture and a block of text side by side, because XWPF has no support for floating (anchored) pictures:
XWPFTable layout = document.createTable(1, 2);
layout.setWidth("100%");
layout.removeBorders();
XWPFParagraph logoCell = layout.getRow(0).getCell(0).getParagraphs().get(0);
Pictures.addScaled(logoCell, Path.of("assets/bookshelf-logo.png"), PictureType.PNG, 4.0, "Bookshelf logo");
XWPFParagraph summaryCell = layout.getRow(0).getCell(1).getParagraphs().get(0);
summaryCell.createRun().setText("Monthly report, October 2026");
A floating picture requires replacing the inline element with wp:anchor through the generated classes in
org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing. There is a worked sample in the POI test sources but
no supported high-level method, so for a cover page or a watermark it is simpler to use a table cell, a header (see
Word Headers, Footers, Sections & Fields) or a
template that already contains the anchored picture.
Pictures and memory
Every picture is read into memory by addPicture, then written to the package part when the document is saved; a
document with fifty full-resolution photographs holds all of them on the heap at the same time. Resize photographs
before adding them (a 4000 by 3000 pixel JPEG shown at 8 cm needs no more than about 1000 pixels of width) and prefer
PNG for charts and logos. When reading, XWPFPictureData reads the part into a byte array, and POI guards the size with
a static limit:
| Setting | Effect |
|---|---|
|
Maximum size in bytes of a single picture part POI will load. Larger parts are rejected when read. Check the current
value with |
Raising a limit in a service that accepts uploaded documents widens the attack surface for decompression bombs, so the limit should be left at its default unless a real file demands more.
Reading pictures from a document
All picture parts of the package are available from the document:
package com.example.bookshelf.reader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFPictureData;
import org.apache.poi.xwpf.usermodel.XWPFRun;
public final class PictureExtractor {
private PictureExtractor() { }
public static int extractAll(Path docx, Path targetDirectory) throws IOException, InvalidFormatException {
Files.createDirectories(targetDirectory);
int count = 0;
try (OPCPackage pkg = OPCPackage.open(docx.toFile());
XWPFDocument document = new XWPFDocument(pkg)) {
for (XWPFPictureData data : document.getAllPictures()) {
// use a generated name: the stored file name is data from the document author
Path target = targetDirectory.resolve("picture-" + (++count) + "." + data.suggestFileExtension());
Files.write(target, data.getData());
}
}
return count;
}
/** Pictures in reading order, with their position in the body. */
public static void list(XWPFDocument document) {
for (XWPFParagraph paragraph : document.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
for (XWPFPicture picture : run.getEmbeddedPictures()) {
System.out.printf("%s (%s) %.0f x %.0f pt%n",
picture.getDescription(),
picture.getPictureData().getPictureTypeEnum(),
picture.getWidth(), picture.getDepth());
}
}
}
}
}
getAllPictures() returns the pictures referenced from the main document, while getAllPackagePictures() also includes
those in headers, footers and other parts. Build file names yourself, as in the example: the name stored in the document
(getFileName()) comes from the author of the file and can contain path separators; writing it unchecked into a target
directory is a path traversal risk for uploaded documents. The security page of the section covers this and other
upload hazards.
What POI does not provide
-
No table of contents or cross-reference helper tied to tables. Captions ("Table 1") are fields; the field API is on the headers and fields page.
-
No cell merge method, no table style builder and no cell border method on the table classes, as shown above. These are written through the
CT...classes. -
No automatic page layout. POI never measures text, so it cannot size a column to its content or tell how many rows fit on a page. Column widths and row heights are values you choose, and
setRepeatHeaderis the only help for long tables. -
No floating pictures, text boxes, SmartArt or shapes in the XWPF model. They can be read and preserved when a document is edited, but not created through the object model.
-
No list-numbering counter values. The numbers shown by Word are computed at render time and are not available to Java code.
-
No picture conversion or resizing. The
ImageIOcall in the example is plain Java, and neither POI nor XWPF converts between image formats. -
No Word-to-PDF rendering. A
.docxwith tables and pictures can be converted only by another tool; the PDF Generation section describes the alternatives to producing the PDF directly.
Pitfalls and checks
| Symptom | Cause and fix |
|---|---|
The picture is tiny or huge. |
The size was passed in pixels or points instead of EMU. Use |
The picture is stretched. |
The width and height do not have the image’s aspect ratio. Compute one from the other, as |
Word reports "unreadable content" after adding a merge. |
A row has fewer or more |
Extra blank line in every table cell. |
|
The table has no borders in Word. |
The default document has no table style. Call the border setters or use a template whose styles exist. |
A numbered list continues from a previous list. |
Both lists share a |
A bullet list shows a box or a wrong glyph. |
The bullet character is not in the level’s font. Use a common character such as the bullet |
The header row does not repeat on page two. |
The row is not among the first rows, or |
|
The bean is not among those in |
|
|
Version notes
| Version | Notes for this page |
|---|---|
POI 4.x |
Picture types were |
POI 5.x (5.5.1 on 2026-10-09) |
The |
POI 6.0 (in development, not released) |
Announced as a Java 17 baseline with the removal of deprecated methods. The |
References
-
Document (Word) components: https://poi.apache.org/components/document/
-
XWPF Quick Guide: https://poi.apache.org/components/document/quick-guide-xwpf.html
-
Document overview: https://poi.apache.org/components/document/docoverview.html
-
Javadoc,
org.apache.poi.xwpf.usermodel: https://poi.apache.org/apidocs/dev/org/apache/poi/xwpf/usermodel/package-summary.html -
Javadoc,
XWPFTable: https://poi.apache.org/apidocs/dev/org/apache/poi/xwpf/usermodel/XWPFTable.html -
Javadoc,
XWPFNumbering: https://poi.apache.org/apidocs/dev/org/apache/poi/xwpf/usermodel/XWPFNumbering.html -
Javadoc,
XWPFRun: https://poi.apache.org/apidocs/dev/org/apache/poi/xwpf/usermodel/XWPFRun.html -
Javadoc,
PictureType: https://poi.apache.org/apidocs/dev/org/apache/poi/common/usermodel/PictureType.html -
Javadoc,
Units: https://poi.apache.org/apidocs/dev/org/apache/poi/util/Units.html -
Official examples: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/
-
ECMA-376 (the WordprocessingML specification): https://ecma-international.org/publications-and-standards/standards/ecma-376/
-
Security: https://poi.apache.org/security.html
-
Release changes: https://poi.apache.org/changes.html