Aspose words python - Missing hyperlink styles in redlined document

Hyperlinks issue document _INPUT.docx (60.9 KB)

Redlined_Hyperlinks_issue_document__OUTPUT.docx (61.8 KB)

Revised_Hyperlinks_issue_document__OUTPUT.docx (55.9 KB)

ISSUE: Able to insert hyperlink but missing styles of hyperlinks fields (both external and internal) in redlining document. But no issue with revised document.

ASPOSE VERSION : 25.7.0

CODE :
Code to insert hyperlink and apply styles :

    def style_hyperlink_field(self, field: aw.fields.Field, style: Dict[str, Any]) -> bool:
        """
        Apply style to all display-text runs of a hyperlink field:
        all runs between field.separator and field.end.
        Returns True if at least one run was updated.
        """
        if field is None:
            return False

        # Check field type defensively
        is_hl = False
        try:
            is_hl = (field.type == aw.fields.FieldType.FIELD_HYPERLINK)
        except Exception:
            is_hl = False
        if not is_hl:
            try:
                code = field.get_field_code() or ""
                is_hl = code.strip().upper().startswith("HYPERLINK")
            except Exception:
                is_hl = False
        if not is_hl:
            return False

        sep = field.separator
        end = field.end
        if sep is None or end is None:
            return False

        node = sep.next_sibling
        updated = False
        while node is not None and node != end:
            if node.node_type == aw.NodeType.RUN:
                self.apply_style_to_font(node.as_run().font, style)
                updated = True
            node = node.next_sibling
        return updated


            URL_PATTERN = re.compile(
                r'\b(?:https?://|www\.)?'
                r'(?:[a-zA-Z0-9-]+\.)+'
                r'(?:[a-zA-Z]{2,})'
                r'(?:[:0-9]*)?'
                r'(?:/[^\s]*)?', re.IGNORECASE
            )
		
	    
	    tokens = ['this is teest link', ' ', 'https://www.google.com', ' ', 'to test links formatting.']
	    orig_token_styles = [('this is teest link', {'text': 'this is teest link', 'bold': False, 'italic': False, 'underline': 'Underline.NONE', 'font_name': 'Arial', 'font_size': 10.0, 'color': None, 'highlight_color': None, 'shading_color': None, 'all_caps': False, 'strike_through': False, 'superscript': False, 'subscript': False, 'hyperlink': False, 'hyperlink_url': None, 'reference_type': None, 'reference_target': None, 'reference_field_code': None, 'footnote': False}), (' ', {}), ('https://www.google.com', {'text': 'https://www.google.com', 'bold': True, 'italic': True, 'underline': 'Underline.SINGLE', 'font_name': 'Times New Roman', 'font_size': 11.0, 'color': 'RGB(0, 0, 255)', 'highlight_color': 'RGB(255, 255, 0)', 'shading_color': None, 'all_caps': False, 'strike_through': False, 'superscript': False, 'subscript': False, 'hyperlink': True, 'hyperlink_url': 'https://www.google.com', 'reference_type': None, 'reference_target': None, 'reference_field_code': 'HYPERLINK "https://www.google.com"', 'footnote': False}), (' ', {'text': ' ', 'bold': True, 'italic': True, 'underline': 'Underline.NONE', 'font_name': 'Times New Roman', 'font_size': 11.0, 'color': 'RGB(112, 48, 160)', 'highlight_color': None, 'shading_color': None, 'all_caps': False, 'strike_through': False, 'superscript': False, 'subscript': False, 'hyperlink': False, 'hyperlink_url': None, 'reference_type': None, 'reference_target': None, 'reference_field_code': None, 'footnote': False}), ('to test links formatting.', {'text': 'to test links formatting.', 'bold': False, 'italic': False, 'underline': 'Underline.NONE', 'font_name': 'Arial', 'font_size': 10.0, 'color': None, 'highlight_color': None, 'shading_color': None, 'all_caps': False, 'strike_through': False, 'superscript': False, 'subscript': False, 'hyperlink': False, 'hyperlink_url': None, 'reference_type': None, 'reference_target': None, 'reference_field_code': None, 'footnote': False})]

	    k = 0
            while k < len(tokens):
                token = tokens[k]
                style = orig_token_styles[k][1] if k < len(orig_token_styles) else {}
                run_hyperlink = style.get("hyperlink", False)
                hyperlink_url = style.get("hyperlink_url")
                field_code = style.get("reference_field_code")
                run_macro = style.get("is_macrobutton", False)


                # External hyperlink handling
                if run_hyperlink and hyperlink_url and URL_PATTERN.match(str(hyperlink_url)):
                    external_link_field = builder.insert_hyperlink(token, hyperlink_url, False)
                    self.style_hyperlink_field(external_link_field, style)

                    k += 1
                    continue

Code to generate redlined and revised document :

        doc.save(str(clean_doc_path))
        print('Saved revised document')

        revised_doc = aw.Document(str(clean_doc_path))
        revised_doc.accept_all_revisions()
       
        # Compare and produce redlined version
        redlined_doc = aw.Document(str(input_doc_path))  # Reload to preserve original for redlining
        redlined_doc.accept_all_revisions()


        opts = aw.comparing.CompareOptions()
        opts.granularity = aw.comparing.Granularity.WORD_LEVEL
        opts.compare_moves = True
        opts.ignore_formatting = False
        opts.ignore_case_changes = False
        opts.ignore_comments = False
        opts.ignore_tables = False
        opts.ignore_fields = False 
        opts.ignore_footnotes = False
        opts.ignore_textboxes = False
        opts.ignore_headers_and_footers = True  # include header/footer TEXT changes.
        
        # Advanced: suppress spurious logo/shape/SDT ID churn in headers/footers
        adv = opts.advanced_options
        adv.ignore_dml_unique_id = True
        adv.ignore_store_item_id = True

        # Compare documents — this adds revisions to original_doc
        redlined_doc.compare(revised_doc, "Redlining", datetime.datetime.now(), opts)

        ro = redlined_doc.layout_options.revision_options
        ro.show_in_balloons = aw.layout.ShowInBalloons.NONE  # renders insert, delete and format inline. 
        ro.inserted_text_color = aw.layout.RevisionColor.CLASSIC_BLUE
        ro.deleted_text_color = aw.layout.RevisionColor.CLASSIC_RED
        ro.show_revision_bars = True
        
        redlined_doc.update_page_layout()
        
        redlined_doc.save(str(redlined_doc_path))

@Sairam_Jyosula

What is happening?

When Aspose.Words generates a red‑lined (compare) document it creates revision nodes for every piece of text that was inserted, deleted or changed.
For a hyperlink field the display text is stored in the runs between the field separator and the field end.
During the compare operation those runs are marked as inserted revisions.

When the revision is rendered (ShowInBalloons = NONE) Aspose.Words applies the revision colour (blue for insertions) instead of the original character formatting that you applied with style_hyperlink_field.
Consequently the hyperlink looks like ordinary text – the underline, colour, font, etc. that you set are no longer visible in the red‑lined output.

The revised document (accept_all_revisions) does not have this problem because the revisions are removed before the document is saved, so the original formatting remains.


How to keep the hyperlink formatting in a red‑lined document

There are three practical ways to solve the issue:

Approach What you do When to use it
A. Keep the formatting on the revision itself Turn on formatting tracking for insertions so that the original character formatting is preserved together with the revision colour. When you want the revision colour and the original formatting to be visible.
B. Ignore the field during comparison Set CompareOptions.ignore_fields = true. The whole field (including its result) is treated as a single atomic block, so the display runs keep their original formatting. When the field code itself does not need to be part of the comparison (most typical scenarios).
C. Re‑apply the hyperlink style after the compare After redlined_doc.compare(...) walk through all FieldHyperlink objects and call your style_hyperlink_field method again. When you need to keep the field code in the comparison and you want the exact same style you used before the compare.

Below you will find code snippets for each approach.


A. Preserve formatting on inserted revisions

# Enable formatting tracking for revisions
ro = redlined_doc.layout_options.revision_options
ro.show_in_balloons = aw.layout.ShowInBalloons.NONE

# <<< NEW SETTINGS >>>
ro.show_formatting = aw.layout.RevisionFormattingMode.APPLY  # keep original formatting
ro.inserted_text_color = aw.layout.RevisionColor.CLASSIC_BLUE
ro.deleted_text_color = aw.layout.RevisionColor.CLASSIC_RED

RevisionFormattingMode.APPLY tells the layout engine to apply the original character formatting (font, colour, underline, etc.) in addition to the revision colour.
Now hyperlinks appear blue and retain their underline/colour.


B. Skip field comparison (simplest fix)

opts = aw.comparing.CompareOptions()
opts.ignore_fields = True          # <-- important
opts.ignore_formatting = False
# …other options unchanged…

redlined_doc.compare(revised_doc, "Redlining", datetime.datetime.now(), opts)

With ignore_fields = true the comparison treats the whole field (including its result) as a single immutable unit.
Since the field result is not examined for differences, Aspose.Words does not create an inserted‑text revision for the runs that contain the hyperlink display text, and the original formatting stays intact.

Note: This option also means that changes inside the hyperlink text (e.g., editing the display text) will not be detected by the compare operation. If you need to detect such edits, use approach C instead.


C. Re‑apply the style after the compare

# 1️⃣ Run the comparison as you already do (ignore_fields = False)
redlined_doc.compare(revised_doc, "Redlining", datetime.datetime.now(), opts)

# 2️⃣ Walk through all hyperlink fields in the red‑lined document
for field in redlined_doc.range.fields:
    if field.type == aw.fields.FieldType.FIELD_HYPERLINK:
        # Retrieve the style you originally used.
        # In this example we store a dict keyed by the hyperlink URL.
        stored_style = hyperlink_style_store.get(field.get_field_code())
        if stored_style:
            style_hyperlink_field(field, stored_style)

# 3️⃣ Save the document
redlined_doc.save(str(redlined_doc_path))

How does it work?
After the compare the document already contains the revision nodes, but the original character formatting is still available on the field result runs (it is simply hidden by the revision colour). By calling style_hyperlink_field again you overwrite the hidden formatting with the style you want, and the revision engine will now render that style together with the revision colour.

Tip: Keep a lookup (hyperlink_style_store in the example) that maps a field’s unique identifier (for example the URL or a custom id you embed in the field code) to the style dictionary you used when inserting the hyperlink. This makes the re‑styling loop deterministic and fast.


Which approach should you pick?

Situation Recommended approach
You do not need to detect changes inside a hyperlink (only want to know that a hyperlink was added/removed) B – ignore_fields (one line change, no extra code)
You want the revision colour and the original formatting to be visible A – keep formatting on revisions
You need the compare operation to detect edits inside the hyperlink text and you want the original formatting to survive C – re‑apply style after compare

Quick checklist for your current code

  1. Set ignore_fields to True if you only care about insertion/deletion of whole hyperlinks.

    opts.ignore_fields = True   # add this line
    
  2. Or, if you prefer to keep the field in the comparison, enable formatting on revisions:

    ro = redlined_doc.layout_options.revision_options
    ro.show_formatting = aw.layout.RevisionFormattingMode.APPLY
    
  3. Or, after compare() re‑apply the style:

    for f in redlined_doc.range.fields:
        if f.type == aw.fields.FieldType.FIELD_HYPERLINK:
            # assuming you saved the original style in a dict keyed by URL
            style = stored_styles.get(f.get_field_code())
            if style:
                self.style_hyperlink_field(f, style)
    
  4. Save the document as you already do.


References

If you run into any trouble while applying one of the above solutions, feel free to reply with a short code snippet showing the exact change you made and the resulting output. I’ll be happy to help you fine‑tune the implementation.

@Sairam_Jyosula Could you please correct me if I am wrong.

Hyperlinks issue document _INPUT.docx is the first document to compare.
Revised_Hyperlinks_issue_document__OUTPUT.docx is the second document to compare.

Is this right? So generally, you do the following:

v1 = aw.Document("C:\\Temp\\Hyperlinks issue document _INPUT.docx")
v1.accept_all_revisions()
v2 = aw.Document("C:\\Temp\\Revised_Hyperlinks_issue_document__OUTPUT.docx")
v2.accept_all_revisions()
v1.compare(v2, "AW", datetime.date.today())
v1.save("C:\\Temp\\out.docx")

The output produced by this code on my side look similar to the Redlined_Hyperlinks_issue_document__OUTPUT.docx you have attached. So could you please clarify what is the expected output you would like to get? As I can see output produced by Aspose.Words is similar to comparison output produced by MS Word:
Aspose.Words: out.docx (57.0 KB)
MS Word: ms.docx (62.9 KB)

In redlined document, Able to retain styles of normal text but not styles of fields (hyperlinks) like highlight color, underlines, text color etc. Expectation is to retain styles.

@Sairam_Jyosula
We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): WORDSNET-28854

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

Can you share details on how to track status of this ticket

@Sairam_Jyosula You can see the issue status at the bottom of this page. Also, once the issue is resolved we will notify you here in this forum thread.

We are not able to see the issue status at the bottom of this page. Can you share a screenshot indicating where to check ?

@Sairam_Jyosula Sure, here is the screenshot: