Find Tag in Template Word Document & Replace with Image at a (x,y) Position using Aspose.Words for Java | Bookmarks

TestAspose.zip (151.6 KB)

Product: Aspose Word for Java version 20.11

Dear Aspose support Team,

We are currently testing the Aspose Word for Java API for one of our project with the trial version and we are in a process to buy a licence,
But we would like to know if the following process is possible with the API:
we have a ODT template document (Template.odt) containing 2 tags %contents% and %Signature%, from this template, we have to generate a document where we replace the %contents% tag with the content of an external DOC/DOCX file (Document_1.docx) and the %Signature% tag by a signature image (Signature.gif), depending on some cases, sometimes we have to insert he signature image only to the last page of the new document generated or sometimes to each page of the new document, is it possible to manage this with the API ?

See code source from the ZIP file, we have already manage to replace the %contents% tag by the contents of the external file but we don’t manage to insert the image at the same position as the %Signature% tag, the image is inserted at the beginning of the page (result.docx), and also if we want to insert the image only at the last page (we use the method moveToDocumentEnd() to go at the end of the document), in this case, there is no image inserted.

Currently, how we have tested to insert the signature image in the final document generated, in first, we replace the tag %Signature% by the corresponding image in the template file and we retrieve the Shape created in order to use the same properties to the shape that we are gonna insert to the final document and it looks like that it is not the good approach and doesn’t work,

See Expected-result_Case1.docx and Expected-result_Case2.docx for expectd document results,

Thank you in advance for your feedback,

@mcouture11,

You can build logic on the following code to get the desired results:

String templateDocument = "C:\\Temp\\TestAspose\\Documents\\Template.odt";
String contentsToInsert = "C:\\Temp\\TestAspose\\Documents\\Document_1.docx";
String signatureImage = "C:\\Temp\\TestAspose\\Documents\\Signature.gif";
String finalDocument = "C:\\Temp\\TestAspose\\Documents\\awjava-20.11.docx";

Document docFinal = new Document(templateDocument);
DocumentBuilder builderFinal = new DocumentBuilder(docFinal);

// Replace %Signature% tag by a bookmark in the template document
FindReplaceOptions optsBookmark = new FindReplaceOptions();
optsBookmark.setDirection(FindReplaceDirection.BACKWARD);
optsBookmark.setReplacingCallback(new ReplacingCallbackImpl());
docFinal.getRange().replace(Pattern.compile("%Signature%"), "", optsBookmark);

// Get the (x, y) coordinates and remove Bookmark
LayoutCollector layoutCollector = new LayoutCollector(docFinal);
LayoutEnumerator layoutEnumerator = new LayoutEnumerator(docFinal);
layoutEnumerator.setCurrent(layoutCollector.getEntity(
        docFinal.getRange().getBookmarks().get("Signature").getBookmarkStart()));
double left = layoutEnumerator.getRectangle().getX();
double top = layoutEnumerator.getRectangle().getY();
docFinal.getRange().getBookmarks().get("Signature").remove();

// Replace the %contents% tag by data from an external document (doc/docx)
FindReplaceOptions opts = new FindReplaceOptions();
opts.setDirection(FindReplaceDirection.BACKWARD);
InsertDocumentAtReplaceHandler insertDocumentAtReplaceHandler = new InsertDocumentAtReplaceHandler();
insertDocumentAtReplaceHandler.setEfindingsToInsert(contentsToInsert);
opts.setReplacingCallback(insertDocumentAtReplaceHandler);
docFinal.getRange().replace(Pattern.compile("%contents%"), "", opts);

//Case 1: Only insert signature at the last page of the document
/*
    builderFinal.moveToDocumentEnd();
    insertShape(builderFinal, signatureImage, left, top);
*/

//Case 2: Insert the signature on each page of the document
NodeCollection runNodes = docFinal.getChildNodes(NodeType.RUN, true);
LayoutCollector collector = new LayoutCollector(docFinal);
int pageIndex = 1;
for (int i = 0; i < runNodes.getCount(); i++) {
    Run run = (Run) runNodes.get(i);
    if (collector.getStartPageIndex(run) == pageIndex) {
        if (run.getAncestor(NodeType.CELL) != null)
            // Let's not anchor image to text inside Cell
            continue;
        builderFinal.moveTo(run);
        insertShape(builderFinal, signatureImage, left, top);
        pageIndex++;
    }
}
docFinal.save(finalDocument);  

public static void insertShape(DocumentBuilder builderFinal, String signatureImage, double left, double top) throws Exception {
    Shape image = builderFinal.insertImage(signatureImage);
    image.setLeft(left);
    image.setTop(top);
    image.setRelativeHorizontalPosition(RelativeHorizontalPosition.PAGE);
    image.setRelativeVerticalPosition(RelativeVerticalPosition.PAGE);
    image.setWrapType(WrapType.NONE);
}

private static Run splitRun(Run run, int position) throws Exception {
    Run afterRun = (Run) run.deepClone(true);
    afterRun.setText(run.getText().substring(position));
    run.setText(run.getText().substring(0, position));
    run.getParentNode().insertAfter(afterRun, run);
    return afterRun;
}

static class ReplacingCallbackImpl implements IReplacingCallback {

    public int replacing(ReplacingArgs e) throws Exception {
        // This is a Run node that contains either the beginning or the complete match.
        Node currentNode = e.getMatchNode();

        // The first (and may be the only) run can contain text before the match,
        // in this case it is necessary to split the run.
        if (e.getMatchOffset() > 0)
            currentNode = splitRun((Run) currentNode, e.getMatchOffset());

        ArrayList runs = new ArrayList();

        // Find all runs that contain parts of the match string.
        int remainingLength = e.getMatch().group().length();
        while ((remainingLength > 0) && (currentNode != null) && (currentNode.getText().length() <= remainingLength)) {
            runs.add(currentNode);
            remainingLength = remainingLength - currentNode.getText().length();

            // Select the next Run node.
            // Have to loop because there could be other nodes such as BookmarkStart etc.
            do {
                currentNode = currentNode.getNextSibling();
            } while ((currentNode != null) && (currentNode.getNodeType() != NodeType.RUN));
        }

        // Split the last run that contains the match if there is any text left.
        if ((currentNode != null) && (remainingLength > 0)) {
            splitRun((Run) currentNode, remainingLength);
            runs.add(currentNode);
        }

        DocumentBuilder builder = new DocumentBuilder((Document) e.getMatchNode().getDocument());
        builder.moveTo((Run) runs.get(0));

        String name = e.getMatch().group(0).substring(1, e.getMatch().group(0).length() - 1);
        builder.startBookmark(name);
        builder.endBookmark(name);

        for (Run run : (Iterable<Run>) runs)
            run.remove();

        // Signal to the replace engine to do nothing because we have already done all what we wanted.
        return ReplaceAction.SKIP;
    }
}

static class InsertDocumentAtReplaceHandler implements IReplacingCallback {

    public static String efindingsToInsert;

    public static void setEfindingsToInsert(String efindingsToInsertTest) {
        efindingsToInsert = efindingsToInsertTest;
    }

    public int replacing(ReplacingArgs e) throws Exception {
        // This is a Run node that contains either the beginning or the complete match.
        Node currentNode = e.getMatchNode();

        // The first (and may be the only) run can contain text before the match,
        // in this case it is necessary to split the run.
        if (e.getMatchOffset() > 0)
            currentNode = splitRun((Run) currentNode, e.getMatchOffset());

        ArrayList runs = new ArrayList();

        // Find all runs that contain parts of the match string.
        int remainingLength = e.getMatch().group().length();
        while ((remainingLength > 0) && (currentNode != null) && (currentNode.getText().length() <= remainingLength)) {
            runs.add(currentNode);
            remainingLength = remainingLength - currentNode.getText().length();

            // Select the next Run node.
            // Have to loop because there could be other nodes such as BookmarkStart etc.
            do {
                currentNode = currentNode.getNextSibling();
            } while ((currentNode != null) && (currentNode.getNodeType() != NodeType.RUN));
        }

        // Split the last run that contains the match if there is any text left.
        if ((currentNode != null) && (remainingLength > 0)) {
            splitRun((Run) currentNode, remainingLength);
            runs.add(currentNode);
        }

        DocumentBuilder builder = new DocumentBuilder((Document) e.getMatchNode().getDocument());
        builder.moveTo((Run) runs.get(0));

        builder.insertDocument(new Document(efindingsToInsert), ImportFormatMode.KEEP_SOURCE_FORMATTING);

        for (Run run : (Iterable<Run>) runs)
            run.remove();

        // Signal to the replace engine to do nothing because we have already done all what we wanted.
        return ReplaceAction.SKIP;
    }
}

Aspose.zip (56.1 KB)

Thank you very much for your feedback,
It works as expected for inserting the image at the correct position on each page but for the case when we need to insert the image on the last page, using moveToDocumentEnd, it inserts nothing. See result attached

//Case 1: Only insert signature at the last page of the document
builderFinal.moveToDocumentEnd();
insertShape(builderFinal, signatureImage, left, top);

@mcouture11,

The problem simply occurs because you are using Aspose.Words for Java in evaluation mode (i.e. without applying a license). I have generated output document (see awjava-20.11.zip (17.6 KB)) with the licensed version of Aspose.Words for Java and do not see this problem on my end. If you want to test ‘Aspose.Words for Java’ without the evaluation version limitations, you can also request a 30-day Temporary License. Please refer to How to get a Temporary License?

Thank you, we have got a Temporary License and made the test, now we can see the image inserted to the last page but it adds an extra blank page, the same as in your output document. We should have 2 pages instead of 3.

resultPage.zip (17.6 KB)

@mcouture11,

The problem occurs because of empty lines (Paragraphs) in template ODT file between %contents% and %Signature% tags. I think, after getting the position of %Signature%, you can remove empty Paragraphs like this:

String templateDocument = "C:\\Temp\\TestAspose\\Documents\\Template.odt";
String contentsToInsert = "C:\\Temp\\TestAspose\\Documents\\Document_1.docx";
String signatureImage = "C:\\Temp\\TestAspose\\Documents\\Signature.gif";
String finalDocument = "C:\\Temp\\TestAspose\\Documents\\awjava-20.11.docx";

Document docFinal = new Document(templateDocument);
DocumentBuilder builderFinal = new DocumentBuilder(docFinal);

// Replace %Signature% tag by a bookmark in the template document
FindReplaceOptions optsBookmark = new FindReplaceOptions();
optsBookmark.setDirection(FindReplaceDirection.BACKWARD);
optsBookmark.setReplacingCallback(new ReplacingCallbackImpl());
docFinal.getRange().replace(Pattern.compile("%Signature%"), "", optsBookmark);

// Get the (x, y) coordinates and remove Bookmark
LayoutCollector layoutCollector = new LayoutCollector(docFinal);
LayoutEnumerator layoutEnumerator = new LayoutEnumerator(docFinal);
layoutEnumerator.setCurrent(layoutCollector.getEntity(
        docFinal.getRange().getBookmarks().get("Signature").getBookmarkStart()));
double left = layoutEnumerator.getRectangle().getX();
double top = layoutEnumerator.getRectangle().getY();
docFinal.getRange().getBookmarks().get("Signature").remove();

// Remove empty Paragraphs
for (Paragraph para : (Iterable<Paragraph>) docFinal.getChildNodes(NodeType.PARAGRAPH, true)) {
    if (para.toString(SaveFormat.TEXT).trim().equals(""))
        para.remove();
}

// Replace the %contents% tag by data from an external document (doc/docx)
FindReplaceOptions opts = new FindReplaceOptions();
opts.setDirection(FindReplaceDirection.BACKWARD);
InsertDocumentAtReplaceHandler insertDocumentAtReplaceHandler = new InsertDocumentAtReplaceHandler();
insertDocumentAtReplaceHandler.setEfindingsToInsert(contentsToInsert);
opts.setReplacingCallback(insertDocumentAtReplaceHandler);
docFinal.getRange().replace(Pattern.compile("%contents%"), "", opts);

// Remove empty Pargaraph(s) from the end
docFinal.getLastSection().getBody().getLastParagraph().remove();

//Case 1: Only insert signature at the last page of the document        
builderFinal.moveToDocumentEnd();
insertShape(builderFinal, signatureImage, left, top);

//Case 2: Insert the signature on each page of the document
/*
    NodeCollection runNodes = docFinal.getChildNodes(NodeType.RUN, true);
    LayoutCollector collector = new LayoutCollector(docFinal);
    int pageIndex = 1;
    for (int i = 0; i < runNodes.getCount(); i++) {
        Run run = (Run) runNodes.get(i);
        if (collector.getStartPageIndex(run) == pageIndex) {
            if (run.getAncestor(NodeType.CELL) != null)
                // Let's not anchor image to text inside Cell
                continue;
            builderFinal.moveTo(run);
            insertShape(builderFinal, signatureImage, left, top);
            pageIndex++;
        }
    }
*/

docFinal.save(finalDocument);

TestAspose.zip (5.5 MB)

Thank you, it works for the example I sent you but when we make the test with a template document wth a background image, the background image is removed in the final document generated. Please see example of template and generated document attached. Thanks

@mcouture11,

Please replace these lines from my previous code:

// Remove empty Paragraphs
for (Paragraph para : (Iterable<Paragraph>) docFinal.getChildNodes(NodeType.PARAGRAPH, true)) {
    if (para.toString(SaveFormat.TEXT).trim().equals(""))
        para.remove();
}

with these:

// Remove empty Paragraphs
for (Section section : docFinal.getSections()) {
    for (Paragraph para : (Iterable<Paragraph>) section.getBody().getChildNodes(NodeType.PARAGRAPH, true)) {
        if (para.toString(SaveFormat.TEXT).trim().equals(""))
            para.remove();
    }
}

result.zip (5.5 MB)

Thank you, the background image is correctly inserted but the textbox in the footer of the document template is displayed behind the Background image in the result file.

We also notcied that if we use the same code for the same documents but with the DOC format instead of DOCX, the background image is not correctly center in the generated document. See files attached

Documents.zip (5.5 MB)

@mcouture11,

Can you please provide new set of documents to be able to reproduce this new issue on our end. Please share the following resources:

  • Template document
  • Document that you want to insert in template
  • The signature image file
  • And Aspose.Words generated output file showing the undesired behavior.

We will then investigate the issue further on our end and provide you more information.

Thank you, please find documents

Documents_1.zip (5.5 MB)

Documents_2.zip (5.5 MB)

@mcouture11,

Please try the following code: (output: Final Word Document in DOC Format.zip (5.5 MB))

String templateDocument = "C:\\Temp\\Documents_1\\Template.odt";
String contentsToInsert = "C:\\Temp\\Documents_1\\Document_To_Insert.doc";
String signatureImage = "C:\\Temp\\Documents_1\\Signature.gif";
String finalDocument = "C:\\Temp\\Documents_1\\awjava-20.11.doc";

Document docFinal = new Document(templateDocument);
DocumentBuilder builderFinal = new DocumentBuilder(docFinal);

// Replace %Signature% tag by a bookmark in the template document
FindReplaceOptions optsBookmark = new FindReplaceOptions();
optsBookmark.setDirection(FindReplaceDirection.BACKWARD);
optsBookmark.setReplacingCallback(new ReplacingCallbackImpl());
docFinal.getRange().replace(Pattern.compile("%Signature%"), "", optsBookmark);

// Get the (x, y) coordinates and remove Bookmark
LayoutCollector layoutCollector = new LayoutCollector(docFinal);
LayoutEnumerator layoutEnumerator = new LayoutEnumerator(docFinal);
layoutEnumerator.setCurrent(layoutCollector.getEntity(
        docFinal.getRange().getBookmarks().get("Signature").getBookmarkStart()));
double left = layoutEnumerator.getRectangle().getX();
double top = layoutEnumerator.getRectangle().getY();
docFinal.getRange().getBookmarks().get("Signature").remove();

// Remove empty Paragraphs
for (Section section : docFinal.getSections()) {
    for (Paragraph para : (Iterable<Paragraph>) section.getBody().getChildNodes(NodeType.PARAGRAPH, true)) {
        if (para.toString(SaveFormat.TEXT).trim().equals(""))
            para.remove();
    }
}

// Replace the %contents% tag by data from an external document (doc/docx)
FindReplaceOptions opts = new FindReplaceOptions();
opts.setDirection(FindReplaceDirection.BACKWARD);
InsertDocumentAtReplaceHandler insertDocumentAtReplaceHandler = new InsertDocumentAtReplaceHandler();
insertDocumentAtReplaceHandler.setEfindingsToInsert(contentsToInsert);
opts.setReplacingCallback(insertDocumentAtReplaceHandler);
docFinal.getRange().replace(Pattern.compile("%contents%"), "", opts);

// Remove empty Pargaraph(s) from the end
docFinal.getLastSection().getBody().getLastParagraph().remove();

//Case 1: Only insert signature at the last page of the document
builderFinal.moveToDocumentEnd();
insertShape(builderFinal, signatureImage, left, top);

//Case 2: Insert the signature on each page of the document
/*
NodeCollection runNodes = docFinal.getChildNodes(NodeType.RUN, true);
LayoutCollector collector = new LayoutCollector(docFinal);
int pageIndex = 1;
for (int i = 0; i < runNodes.getCount(); i++) {
Run run = (Run) runNodes.get(i);
if (collector.getStartPageIndex(run) == pageIndex) {
    if (run.getAncestor(NodeType.CELL) != null)
        // Let's not anchor image to text inside Cell
        continue;
    builderFinal.moveTo(run);
    insertShape(builderFinal, signatureImage, left, top);
    pageIndex++;
}
}
*/

// Save to tempprary DOCX stream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
docFinal.save(baos, SaveFormat.DOCX);
// Save to DOC format
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Document doc = new Document(bais);
doc.save(finalDocument);

Thank you, it is ok with the last code for the DOC format but there is still the issue with the Textbox in the footer which is displayed behind the background image in the final document generated

image.jpg (146.7 KB)

image.jpg (54.0 KB)

Hello, we have tested with a document with 10 pages and we have 3 issues:

  • the generated document has 11 pages instead of 10 pages
  • The signature image is not inserted to all pages, it stops at page 5.
  • Textbox in the footer is displayed behind the background image
    Please find documents used and generated and source code. Thanks

Documents_part1.zip (5.4 MB)
Documents_part2.zip (5.5 MB)

@mcouture11,

When we open “Template.odt” with OpenOffice Writer, it displays textbox behind the background image. However, MS Word 2019 displays the textbox in “Template.odt” in front of the background image.

When we convert “Template.odt” to DOC format by using Aspose.Words, we observe the issue of the background image not being correctly centered. Converting “Template.odt” to DOCX format produces well centered image but again textbox goes behind background image. For the sake of any correction in Aspose.Words API, we have logged this problem in our issue tracking system with ID WORDSNET-21517. We will further look into the details of this problem and will keep you updated on the status of correction. We apologize for your inconvenience.

Hello, thank you for your feedback, I have just reported another problem with another document, thanks.

@mcouture11,

We are checking this scenario and will get back to you soon.

@mcouture11,

It is to inform you that we have also logged following problem in our issue tracking system:

  • WORDSNET-21581: Unable to insert signature Shapes at specified position at all Pages

@mcouture11,

Regarding WORDSNET-21581, please check the following analysis details:

There is a different document layout in the ‘Template’ and inserted document. Margins are almost the same (but still not the same), but header size is different - 50,8 pt and 34 pt. It would be difficult to have the same number of pages when you inserts one document into another unless documents are absolutely identical. We suggest you to use page breaks to control page split (count).

For this particular case, please do the following changes:

Remove that condition at all:

if (run.getAncestor(NodeType.CELL) != null)
    // Let's not anchor image to text inside Cell
    continue;

And change Shape insertShape method like this

image.isLayoutInCell(false);
image.setWidth(222);
image.setHeight(58);

Complete Java code is as follows:

String templateDocument = "C:\\Temp\\Documents_part1\\Template.odt";
String contentsToInsert = "C:\\Temp\\Documents_part1\\DocumentToInsert.doc";
String signatureImage = "C:\\Temp\\Documents_part1\\Signature.gif";
String finalDocument = "C:\\Temp\\Documents_part1\\awjava-21.1.doc";

Document docFinal = new Document(templateDocument);
DocumentBuilder builderFinal = new DocumentBuilder(docFinal);

// Replace %Signature% tag by a bookmark in the template document
FindReplaceOptions optsBookmark = new FindReplaceOptions();
optsBookmark.setDirection(FindReplaceDirection.BACKWARD);
optsBookmark.setReplacingCallback(new ReplacingCallbackImpl());
docFinal.getRange().replace(Pattern.compile("%Signature%"), "", optsBookmark);

// Get the (x, y) coordinates and remove Bookmark
LayoutCollector layoutCollector = new LayoutCollector(docFinal);
LayoutEnumerator layoutEnumerator = new LayoutEnumerator(docFinal);
layoutEnumerator.setCurrent(layoutCollector.getEntity(
        docFinal.getRange().getBookmarks().get("Signature").getBookmarkStart()));
double left = layoutEnumerator.getRectangle().getX();
double top = layoutEnumerator.getRectangle().getY();
docFinal.getRange().getBookmarks().get("Signature").remove();

// Remove empty Paragraphs
for (Section section : docFinal.getSections()) {
    for (Paragraph para : (Iterable<Paragraph>) section.getBody().getChildNodes(NodeType.PARAGRAPH, true)) {
        if (para.toString(SaveFormat.TEXT).trim().equals(""))
            para.remove();
    }
}

// Replace the %contents% tag by data from an external document (doc/docx)
FindReplaceOptions opts = new FindReplaceOptions();
opts.setDirection(FindReplaceDirection.BACKWARD);
InsertDocumentAtReplaceHandler insertDocumentAtReplaceHandler = new InsertDocumentAtReplaceHandler();
insertDocumentAtReplaceHandler.setEfindingsToInsert(contentsToInsert);
opts.setReplacingCallback(insertDocumentAtReplaceHandler);
docFinal.getRange().replace(Pattern.compile("%contents%"), "", opts);

// Remove empty Pargaraph(s) from the end
docFinal.getLastSection().getBody().getLastParagraph().remove();

////////////////////////////////////////////////////////
//Case 1: Only insert signature at the last page of the document
//  builderFinal.moveToDocumentEnd();
//  insertShape(builderFinal, signatureImage, left, top);
////////////////////////////////////////////////////////

////////////////////////////////////////////////////////
//Case 2: Insert the signature on each page of the document

NodeCollection runNodes = docFinal.getChildNodes(NodeType.RUN, true);
LayoutCollector collector = new LayoutCollector(docFinal);
int pageIndex = 1;
for (int i = 0; i < runNodes.getCount(); i++) {
    Run run = (Run) runNodes.get(i);
    if (collector.getStartPageIndex(run) == pageIndex) {
        builderFinal.moveTo(run);
        insertShape(builderFinal, signatureImage, left, top);
        pageIndex++;
    }
}
////////////////////////////////////////////////////////

// Save to tempprary DOCX stream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
docFinal.save(baos, SaveFormat.DOCX);
// Save to DOC format
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Document doc = new Document(bais);
doc.save(finalDocument);

public static Shape insertShape(DocumentBuilder builderFinal, String signatureImage, double left, double top) throws Exception {
    Shape image = builderFinal.insertImage(signatureImage);
    image.setLeft(left);
    image.setTop(top);
    image.setRelativeHorizontalPosition(RelativeHorizontalPosition.PAGE);
    image.setRelativeVerticalPosition(RelativeVerticalPosition.PAGE);
    image.setWrapType(WrapType.NONE);
    image.setZOrder(10);

    image.isLayoutInCell(false);
    image.setWidth(222);
    image.setHeight(58);

    return image;
}

private static Run splitRun(Run run, int position) throws Exception {
    Run afterRun = (Run) run.deepClone(true);
    afterRun.setText(run.getText().substring(position));
    run.setText(run.getText().substring(0, position));
    run.getParentNode().insertAfter(afterRun, run);
    return afterRun;
}

static class ReplacingCallbackImpl implements IReplacingCallback {

    public int replacing(ReplacingArgs e) throws Exception {
        // This is a Run node that contains either the beginning or the complete match.
        Node currentNode = e.getMatchNode();

        // The first (and may be the only) run can contain text before the match,
        // in this case it is necessary to split the run.
        if (e.getMatchOffset() > 0)
            currentNode = splitRun((Run) currentNode, e.getMatchOffset());

        ArrayList runs = new ArrayList();

        // Find all runs that contain parts of the match string.
        int remainingLength = e.getMatch().group().length();
        while ((remainingLength > 0) && (currentNode != null) && (currentNode.getText().length() <= remainingLength)) {
            runs.add(currentNode);
            remainingLength = remainingLength - currentNode.getText().length();

            // Select the next Run node.
            // Have to loop because there could be other nodes such as BookmarkStart etc.
            do {
                currentNode = currentNode.getNextSibling();
            } while ((currentNode != null) && (currentNode.getNodeType() != NodeType.RUN));
        }

        // Split the last run that contains the match if there is any text left.
        if ((currentNode != null) && (remainingLength > 0)) {
            splitRun((Run) currentNode, remainingLength);
            runs.add(currentNode);
        }

        DocumentBuilder builder = new DocumentBuilder((Document) e.getMatchNode().getDocument());
        builder.moveTo((Run) runs.get(0));

        String name = e.getMatch().group(0).substring(1, e.getMatch().group(0).length() - 1);
        builder.startBookmark(name);
        builder.endBookmark(name);

        for (Run run : (Iterable<Run>) runs)
            run.remove();

        // Signal to the replace engine to do nothing because we have already done all what we wanted.
        return ReplaceAction.SKIP;
    }
}

static class InsertDocumentAtReplaceHandler implements IReplacingCallback {

    public static String efindingsToInsert;

    public static void setEfindingsToInsert(String efindingsToInsertTest) {
        efindingsToInsert = efindingsToInsertTest;
    }

    public int replacing(ReplacingArgs e) throws Exception {
        // This is a Run node that contains either the beginning or the complete match.
        Node currentNode = e.getMatchNode();

        // The first (and may be the only) run can contain text before the match,
        // in this case it is necessary to split the run.
        if (e.getMatchOffset() > 0)
            currentNode = splitRun((Run) currentNode, e.getMatchOffset());

        ArrayList runs = new ArrayList();

        // Find all runs that contain parts of the match string.
        int remainingLength = e.getMatch().group().length();
        while ((remainingLength > 0) && (currentNode != null) && (currentNode.getText().length() <= remainingLength)) {
            runs.add(currentNode);
            remainingLength = remainingLength - currentNode.getText().length();

            // Select the next Run node.
            // Have to loop because there could be other nodes such as BookmarkStart etc.
            do {
                currentNode = currentNode.getNextSibling();
            } while ((currentNode != null) && (currentNode.getNodeType() != NodeType.RUN));
        }

        // Split the last run that contains the match if there is any text left.
        if ((currentNode != null) && (remainingLength > 0)) {
            splitRun((Run) currentNode, remainingLength);
            runs.add(currentNode);
        }

        DocumentBuilder builder = new DocumentBuilder((Document) e.getMatchNode().getDocument());
        builder.moveTo((Run) runs.get(0));

        builder.insertDocument(new Document(efindingsToInsert), ImportFormatMode.KEEP_SOURCE_FORMATTING);

        for (Run run : (Iterable<Run>) runs)
            run.remove();

        // Signal to the replace engine to do nothing because we have already done all what we wanted.
        return ReplaceAction.SKIP;
    }
}