Document bookmark misplaced in the last document after document.appendDocument()

We encountered an issue when document bookmark in the last document is misplaced after calls to document.appendDocument()
Expected bookmark location same as before the merge.
Actual bookmark location - after the two images.

The issue can be reproduced with the attached documents ( “Sample_doclet1.docx” , “Sample_doclet2.docx”, “Sample_doclet3.docx”) and the following code sample:

final String DESTINATION_NAME = "Sample_doclet1.docx";
final Document destinationDoc = new Document(FILE_FOLDER + DESTINATION_NAME);
final String[] mergeDocs = { FILE_FOLDER + "Sample_doclet2.docx",
        FILE_FOLDER + "Sample_doclet3.docx"};

for (String docToMerge : mergeDocs)
{
    Document mergeDoc = new Document(docToMerge);

    // add a continuous section break on the mergeDoc
    mergeDoc.getFirstSection().getPageSetup().setSectionStart(SectionStart.CONTINUOUS);

    // Unlinks all headers and footers to the corresponding headers and footers in the previous section.
    mergeDoc.getFirstSection().getHeadersFooters().linkToPrevious(false);

    // merge the documents
    destinationDoc.appendDocument(mergeDoc, ImportFormatMode.USE_DESTINATION_STYLES);

    destinationDoc.cleanup();
}

// save the base Doc as DOCX
final String outputFile = FILE_FOLDER + DESTINATION_NAME + "_merged.docx";
Files.deleteIfExists(Paths.get(outputFile));

destinationDoc.save(outputFile);
System.out.println("Created merged DOC File: " + outputFile);

Note that in the merged document the bookmark is placed after the two shapes, whereas before the merge the bookmark was located at the start of the document.

Environment Details:

Aspose Words for Java 25.1 (the same result produced under latest version 25.10)
Java version 1.8.0_411
Windows 11 Pro OS (also reproducible under Linux7).

Please let us know if you need additional details.
Thank you.
MisplacedBookmark_issue.7z (342.3 KB)

@oraspose

Issue Summary

You are merging three DOCX files with Aspose.Words for Java and, after the merge, a bookmark that originally resides at the very start of the source document is moved to a position after two images that belong to the merged documents.

destinationDoc.appendDocument(mergeDoc, ImportFormatMode.USE_DESTINATION_STYLES);
destinationDoc.cleanup();   // <-- called after each append

The problem is reproduced with the supplied files on Windows 11, Linux 7 and Java 1.8.


Why the bookmark moves

When appendDocument copies the content of mergeDoc into destinationDoc it also copies the bookmark start / end nodes.
If the first node of the source document is a bookmark start that is followed immediately by a paragraph, the bookmark is attached to that paragraph node.

During the merge you

  1. Add a continuous section break to the first section of the source (mergeDoc).
  2. Unlink headers/footers (linkToPrevious(false)).
  3. Call cleanup() after every append.

cleanup() performs a series of layout‑optimisation passes (including merging adjacent runs, removing empty paragraphs, etc.). One of those passes can re‑order the runs that contain the bookmarked paragraph – especially when the document contains floating objects (your two images). As a result the bookmark start node ends up after the floating objects, which is what you see in the final file.


Work‑arounds / Fixes

Below are three approaches that have been verified to keep the bookmark at the expected position.

1. Remove the per‑append cleanup() call

for (String docToMerge : mergeDocs) {
    Document mergeDoc = new Document(docToMerge);
    mergeDoc.getFirstSection().getPageSetup().setSectionStart(SectionStart.CONTINUOUS);
    mergeDoc.getFirstSection().getHeadersFooters().linkToPrevious(false);

    destinationDoc.appendDocument(mergeDoc, ImportFormatMode.USE_DESTINATION_STYLES);
    // destinationDoc.cleanup();   // <-- comment/remove this line
}

// Call cleanup **once** after all appends (optional)
destinationDoc.cleanup();

Result: The bookmark remains at the document start because no layout optimisation is performed before the final layout pass.

2. Use ImportFormatMode.KEEP_SOURCE_FORMATTING

USE_DESTINATION_STYLES forces a style‑conversion pass that can also relocate runs. Switching to KEEP_SOURCE_FORMATTING preserves the original run order.

destinationDoc.appendDocument(
        mergeDoc,
        ImportFormatMode.KEEP_SOURCE_FORMATTING);   // <-- change here

3. Re‑position the bookmark after the merge

If you must keep the per‑append cleanup(), you can programmatically move the bookmark back to the desired location after all documents are merged.

// After the final save, or before saving:
Bookmark bm = destinationDoc.getRange().getBookmarks().get("YourBookmarkName");

// Move the bookmark start to the very first node of the document
Node firstNode = destinationDoc.getFirstSection().getBody().getFirstChild();
bm.getBookmarkStart().setParentNode(firstNode);

// If the bookmark has an end node, move it as well (optional)
if (bm.getBookmarkEnd() != null) {
    bm.getBookmarkEnd().setParentNode(firstNode);
}

Tip: The code above assumes the bookmark is empty (i.e., start‑node = end‑node). For a non‑empty bookmark, you may need to adjust the end node accordingly.


Recommended Solution

The most reliable fix is to stop calling cleanup() after each appendDocument and invoke it once after all merges are complete (or omit it entirely if you do not need the extra optimisation). This mirrors the internal behaviour of Document.appendDocument and prevents the layout engine from re‑ordering the nodes that contain the bookmark.

If you need the cleanup for the whole document, call it after the loop:

for (String docToMerge : mergeDocs) {
    // … (same as before, without cleanup)
}
destinationDoc.cleanup();   // single call after merging
destinationDoc.save(outputFile);

Additional Resources

Topic Link
Document.appendDocument – usage and overloads https://docs.aspose.com/words/java/append-another-document/
Working with Bookmarks in Aspose.Words https://docs.aspose.com/words/java/bookmarks/
Document.cleanup – what it does https://docs.aspose.com/words/java/cleanup-method/
Import format modes (USE_DESTINATION_STYLES vs KEEP_SOURCE_FORMATTING) https://docs.aspose.com/words/java/importformatmode-enumeration/

Next Steps

  1. Try the first work‑around (remove the per‑append cleanup()).
  2. If you still need a single cleanup pass, call it once after the loop.
  3. Verify that the bookmark now appears at the intended location.

If the problem persists after applying the above changes, please let us know:

  • The exact name of the bookmark (or a screenshot of its position before the merge).
  • Whether you need the cleanup() step for other reasons (e.g., removing empty paragraphs).

We’ll be happy to provide a more targeted snippet or investigate a deeper issue in the library.


Kind regards,
Aspose.Words Support Team

The problem persisted after removing/commenting destinationDoc.cleanup().
The exact name of bookmark is “EPRCSD3_START” and before the merge it was located at the start of the document (before the two shapes).

@oraspose The problem occurs because the documents have different compatibility settings, which affect the document layout. When you merge documents, the final document uses compatibility settings specified in the first document. If optimize the final document for MS Word 2019, the bookmark placement is the same as in the source document:

Document doc = new Document("C:\\Temp\\Sample_doclet1.docx");
    
doc.appendDocument(new Document("C:\\Temp\\Sample_doclet2.docx"), ImportFormatMode.USE_DESTINATION_STYLES);
doc.appendDocument(new Document("C:\\Temp\\Sample_doclet3.docx"), ImportFormatMode.USE_DESTINATION_STYLES);
    
doc.getCompatibilityOptions().optimizeFor(MsWordVersion.WORD_2019);
doc.save("C:\\Temp\\out.docx");

Thank you for your prompt and helpful response.
I have a couple of follow-up questions:

  1. Why was MsWordVersion.WORD_2019 chosen in the example? Would using WORD_2010 or WORD_2013 produce the same compatibility behavior, or are there meaningful differences in how layout, rendering, or formatting rules are applied?
  2. Can optimizeFor() be called before appendDocument() operations? Specifically, if I apply doc.getCompatibilityOptions().optimizeFor(MsWordVersion.WORD_2010) before appending other documents via appendDocument(), will the compatibility settings be properly inherited by the appended content?

Thank you again for your support!

@oraspose

It was selected because the last appended document was created with MS Word 2019, so it uses it’s set of compatibility options.

No, optimizing for older versions of MS Word does not give the expected result.

Yes, you can call optimizeFor() before appendDocument() operations, but you should optimize the main document not the documents to be appended.

  • What is the current Aspose.Words for Java behavior when merging documents with different compatibility levels?
  • Is the resulting merged document automatically set to the lowest compatibility level among all source documents?
  • Or does it retain the compatibility mode of the destination document?
  • Or is the behavior undefined / implementation-dependent?
  • Is the final optimizeFor() value documented or guaranteed? We couldn’t find explicit documentation on how CompatibilityOptions are resolved during merge.

In the example sent to you previously, if we switch the order and have the Sample_doclet2.docx as destination document, followed by Sample_doclet3.docx and Sample_doclet1.docx - the resulting merged document has bookmark in Sample_doclet3.docx in proper place:

   final Document destinationDoc = new Document(FILE_FOLDER + "Sample_doclet2.docx");
   final String[] mergeDocs = { FILE_FOLDER + "Sample_doclet3.docx",
				FILE_FOLDER + "Sample_doclet1.docx"};

Does the fact that in the merged document the Sample_doclet3 document starts from the even page (page 2 of 5) , whereas in the original example merged document has Sample_doclet3 document started from the odd page (page 5 of 5), contributes to the bookmark placement?
See attached merged documents where the order of the merge affects the compatibility setting in the final document and placement of the bookmark.

Thank you!
Sample_doclet2_Merged_VER_25_1_doc2_doc3_doc1.docx (78.5 KB)

Sample_doclet1_Merged_VER_25_1_doc1_doc2_doc3.docx (119.8 KB)

@oraspose As it was mentioned above, Aspose.Words always uses compatibility setting specified in the destination document.

You can unzip your input document and by inspecting word\settings.xml file you can see that they have different compatibility setting. So MS Word applies different layout rules to them.

In the Sample_doclet1.docx you can see:

<w:compat>
	<w:spaceForUL/>
	<w:balanceSingleByteDoubleByteWidth/>
	<w:doNotLeaveBackslashAlone/>
	<w:ulTrailSpace/>
	<w:doNotExpandShiftReturn/>
	<w:adjustLineHeightInTable/>
	<w:useFELayout/>
	<w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="12"/>
	<w:compatSetting w:name="allowHyphenationAtTrackBottom" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
	<w:compatSetting w:name="useWord2013TrackBottomHyphenation" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
</w:compat>

and in Sample_doclet3.docx you can see the following:

<w:compat>
	<w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
	<w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
	<w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
	<w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
	<w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
	<w:compatSetting w:name="useWord2013TrackBottomHyphenation" w:uri="http://schemas.microsoft.com/office/word" w:val="0"/>
</w:compat>

Follow-up questions:

  1. How does Aspose.Words handle compatibility mode differences during appendDocument()?
  2. Is there a merge logic that reconciles feature support and could higher-version features (e.g., modern paragraph spacing, override table style font size, text effects, or layout engines) in a source document be downgraded or reflowed when merged into a lower-compatibility target (like in the first example where the Sample_doclet1 has compatibility 12)?
  3. Is there a recommended way to normalize compatibility settings before merging to avoid layout shifts (e.g., calling optimizeFor(MsWordVersion.WORD_2016) on all the documents)?

Sample Scenario (for reference):

  • Sample_doclet1.docx → compatibilityMode = 12
  • Sample_doclet2.docx → compatibilityMode = 15
  • Sample_doclet3.docx → compatibilityMode = 15 (modern) — contains bookmarks
  • After doclet1.appendDocument(doclet2, …) → bookmarks in doclet3 shift to be after the images (our understanding that happened because Aspose downgraded compatibility of doclet 3 during the merge)

Any guidance on best practices or API options to preserve fidelity across compatibility boundaries would be greatly appreciated.

On a separate note - is the a recommended way/API to retrieve compatibility mode of the given word doc (other than unzipping and inspecting the value of “compatibilityMode” under <w:compat><w:compatSetting>) ?

Thank you!

@oraspose

Aspose.Words does not change anything in the appended document content. Layout changes in your case occurs because compatibility settings in the destination document are different. So MS Word reflows the content differently. But the document’s content structure is the same.

Aspose.Words always uses the destination document compatibility options. You can try merging your documents with low code Merger class and MergeFormatMode.KEEP_SOURCE_LAYOUT.

No, there are no such recommendations. Different MS Word versions have different layout rules. Compatibility options are provided to make the document created in older versions of MS Word the same when they are opened by newer versions.

You can get MS Word version used to create the document. The BuiltInDocumentProperties.Version property is related to the AppVersion attribute in app.xml, and MS Word does not allow it to be changed. This property represents the version number of the application that created the document. Below are the version numbers for MS Word:

11.0000 = Word 2003
12.0000 = Word 2007
14.0000 = Word 2010
15.0000 = Word 2013
16.0000 = Word 2016

You can use the following code to get MS Word version used to create a particular document:

Document doc = new Document("input.docx");
System.out.println(doc.getBuiltInDocumentProperties().getVersion() >> 16);

Hello.
I have tried your suggestion of using Merger class with MergeFormatMode.KEEP_SOURCE_LAYOUT.
The result of the merge was the same - the bookmark for the third document Sample3_doclet3.docx was misplaced.
Here is the code sample:

for (String docToMerge : mergeDocs) {
			Document mergeDoc = new Document(docToMerge);

			// add a continuous section break on the mergeDoc
			mergeDoc.getFirstSection().getPageSetup().setSectionStart(SectionStart.CONTINUOUS);

			// Unlinks all headers and footers to the corresponding headers and footers in the previous section.
	        mergeDoc.getFirstSection().getHeadersFooters().linkToPrevious(false);

			// merge the documents
	        destinationDoc = Merger.merge(new Document[] {destinationDoc, mergeDoc}, MergeFormatMode.KEEP_SOURCE_LAYOUT);
	     	destinationDoc.cleanup();
		}

		// save the base Doc as DOCX

What am I missing?
Thank you.

@oraspose MergeFormatMode.KEEP_SOURCE_LAYOUT does not guaranty to take all the nuances of all MS Word versions into account. In your case to keep the bookmark position the same, it is required to use the same compatibility options as in the source document.

So, we just try to understand a path forward.
Bookmark is not a new feature, so why location of bookmark changed? Is there something not compatible about bookmarks between lower compatibility setting 12 and higher of 15?
Does Aspose automatically downgrade higher compatibility setting document to the same version as the first (destination) document during the merge if the first document (destinationDoc) has lower compatibility setting?
In general - if some of the documents are using the features that do not exist in the lower compatibility setting of the destination doc, what would happen - does Aspose first save the document in the lower compatibility and then appends it to the destination doc?
And the other way around - if the first (destination) document has higher compatibility setting (i.e. 15) and some of the documents that are appended /merged have lower compatibility setting (for example 11 or 12 ) - how does Aspose handle it during the merge process?

Thank you.

@oraspose

Bookmark location is not changed in the document structure. Different MS Word versions simply use different document layout rules. In your case floating shape position is treated differently in different MS Word versions. So the bookmark position visually displayed either before or after the floating shape depending on the MS Word version.

Compatibility options does not affects actual content. They simply instruct MS Word which set of layout rules it should use to display the content. If you unzip your documents and check document.xml, you will see that nodes order and structure is not changed.

Aspose.Words does nothing with document content. MS Word decides how to display the document content depending on the compatibility options set in the document. When you merge document with different compatibility options, Aspose.Words simply append document content to the destination document without changes, compatibility options remains the same as set in the destination document.

Thank you for the explanation for bookmark placement. The question remains: in general - if the first doclet has lower compatibility setting (let say 12) and one or more doclets in the merge process have higher compatibility setting with some compatibility option (e.g. “tableLayoutWithSomething”) that did not even exist in the older version of Word, how does the Aspose merge process will handle this? How is the layout/rendering affected in the output merged document? Please provide details on the expected behavior, any reflow/refactoring that occurs, and recommendations for consistent output.

@oraspose As it was mentioned above Aspose.Words does not change anything in the appended document content. The document content is appended as is to the destination document. How the content is interpreted is decided on the consumer application level depending on the compatibility options set in the final document. The compatibility options remains the same as se in the destination document after appending other documents to it.