<p><a href="https://github.com/moshi4/pyMSAviz" rel="nofollow noreferrer">pyMSAviz</a> may help with this problem.
pyMSAviz is a Python tool with a convenient CLI/API implementation for visualizing MSA files.</p>
<p>For example, you can output MSA figure in PDF format with the following command.</p>
<pre><code>pymsaviz -i Multi.txt -o msa_result.pdf --format clustal \
         --wrap_length 60 --show_count --color_scheme Identity
</code></pre>
<p><a href="https://i.sstatic.net/xTbPh.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/xTbPh.png" alt="msa_result.png" /></a></p>
<p>You can also use pyMSAviz's API to configure the appearance of the figure in more detail, such as highlighting.</p>

CC BY-SA 4.0
2022-12-21T14:05:51.040Z
Thanks for your solution Moshi. I personally think @pippo1980's solution is cooler because they leverage AlignIO within the StringIO (or StringIO within AlignIO). This permits a lot of flexibility in possible solutions because we use BioPython (AlignIO) all the time and is very cool OO.
M__
CC BY-SA 4.0
2022-12-22T00:12:18.413Z
personally I think your tool is supercool, spent last 3 days googling for such thing, but wasnt able to find anything ( I mean espript3 is supersupercool like jalview but no python API ... could be wrong). Wasted a day trying to understand how to get PyMuPDF  to find words, like 'ggaag' in my out.pdf with no luck. I am intrigued by your tool. Is there a way to color conserved residues all blue and non conserved like red ?? Was thinking about how to realize such a thing, Pyfpdf2 can highligh single cell and I should find a way to print each single alignment letter in a single cell and devising
pippo1980
CC BY-SA 4.0
2022-12-22T00:13:32.627Z
a strategy to do that. Sorry I cannot upvote more than once.
pippo1980
CC BY-SA 4.0
2022-12-22T00:34:31.563Z
yep found  --color_scheme Identity in your examples !!!
pippo1980
CC BY-SA 4.0
2022-12-24T01:02:16.517Z
Hi @pippo1980. I'm pretty sure you could get yours working. Note that MEGA X alignment editor will colour conserved residues (but you don't get to choose the colour) and can't do non-conserved residues. Merry Christmas!
M__
CC BY-SA 4.0
2022-12-21T10:20:09.240Z
2022-12-21T10:20:09.240Z
moshi
<p>adding, a second answer because the OP request about</p>
<blockquote>
<p>I need to import the file and then highlight some specific word</p>
</blockquote>
<p>was making me uneasy. I kept using the <code>Pyfpdf2</code> library and made use of the Biopython <code>SeqRecord.letter_annotations</code> attribute to track Nucleotides index composing the words found by RegEx. Tried to use the <code>page.search_for()</code> of the <code>PyMuPDF</code> library directly on the output pdf of my previous answer but was not getting word accross line breaks [if anybody know better, please help].</p>
<p>Here my code, it's a two half problem, first find the word (in my example is '<em>atat</em>'), then write everything to pdf, I stole it from <a href="https://github.com/biopython/biopython/blob/8d9d20d09c6ee2a672c8477e890f0ab4e9e48825/Bio/AlignIO/ClustalIO.py#L22" rel="nofollow noreferrer">from /master/Bio/AlignIO/ClustalIO.py/   def write_alignment(self, alignment): from class ClustalWriter(SequentialAlignmentWriter)</a> with some modding [I believe but not absolutely sure that ...A cell by definition is uniform in font and color... ]:</p>
<pre><code>import re

from Bio import AlignIO  # Biopython 1.80

from fpdf import FPDF # pip install fpdf2
 

alignment = AlignIO.read(&quot;Multi.txt&quot;, &quot;clustal&quot;)


word_to_find = 'atat'

for rec in alignment:
    
    rec.letter_annotations['highlight'] = 'N'*len(rec.seq)

# for rec in alignment:    
#     print('rec.id : ', rec.id, 'highlight : ', rec.letter_annotations['highlight'])
    
    
for rec in alignment:
    
    # print(str(rec.seq))
    
    found = [m.start() for m in re.finditer( '(?='+re.escape(word_to_find)+')' , re.escape(str(rec.seq.replace('-', 'Z'))))]
    
    # print(rec.id , ' found : ', found)
    
    for i in found :
        
            a = list(rec.letter_annotations['highlight'])
    
            a[i:i+len(word_to_find)] = 'Y'*len(word_to_find)
            
            rec.letter_annotations['highlight'] = ''.join(i for i in a)
            
            # print(rec.letter_annotations['highlight'])
            
            

max_length = alignment.get_alignment_length()


# stolen from /master/Bio/AlignIO/ClustalIO.py/   def write_alignment(self, alignment): from class ClustalWriter(SequentialAlignmentWriter):
def write_alignment(alignment):
    
        x = 2
        
        y = 2

        pdf = FPDF(orientation=&quot;P&quot;, unit=&quot;mm&quot;, format=&quot;A4&quot;)
         
        # Add a page
        pdf.add_page()

        pdf.add_font('FreeMono', '', 'FreeMono.ttf')

        pdf.set_font(&quot;FreeMono&quot;, size = 12)
        
        pdf.set_fill_color(255, 255, 0)

    
        &quot;&quot;&quot;Use this to write (another) single alignment to an open file.&quot;&quot;&quot;
        if len(alignment) == 0:
            raise ValueError(&quot;Must have at least one sequence&quot;)
        if alignment.get_alignment_length() == 0:
            # This doubles as a check for an alignment object
            raise ValueError(&quot;Non-empty sequences are required&quot;)

        # Old versions of the parser in Bio.Clustalw used a ._version property
        try:
            version = str(alignment._version)
        except AttributeError:
            version = &quot;&quot;
        if not version:
            version = &quot;1.81&quot;
        if version.startswith(&quot;2.&quot;):
            # e.g. 2.0.x
            output = f&quot;CLUSTAL {version} multiple sequence alignment&quot;
        else:
            # e.g. 1.81 or 1.83
            output = f&quot;CLUSTAL X ({version}) multiple sequence alignment&quot;
            
        pdf.cell(0, y, txt = output , border = 0,  new_x='LMARGIN'  , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)
        pdf.cell(0, 4*y, txt = '' , border = 0,  new_x='LMARGIN'  , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)
        

        cur_char = 0
        # # max_length = len(alignment[0])
        max_length = alignment.get_alignment_length()


        if max_length &lt;= 0:
            raise ValueError(&quot;Non-empty sequences are required&quot;)

        if &quot;clustal_consensus&quot; in alignment.column_annotations:
            star_info = alignment.column_annotations[&quot;clustal_consensus&quot;]
        else:
            try:
                # This was originally stored by Bio.Clustalw as ._star_info
                star_info = alignment._star_info
            except AttributeError:
                star_info = None

        # keep displaying sequences until we reach the end
        
        rec_cnt = 0
        
        while cur_char != max_length:
            # calculate the number of sequences to show, which will
            # be less if we are at the end of the sequence
            if (cur_char + 50) &gt; max_length:
                show_num = max_length - cur_char
            else:
                show_num = 50

            # go through all of the records and print out the sequences
            # when we output, we do a nice 80 column output, although this
            # may result in truncation of the ids.
            for record in alignment:
                
                rec_cnt += 1 
                
                # Make sure we don't get any spaces in the record
                # identifier when output in the file by replacing
                # them with underscores:
                line = record.id[0:30].replace(&quot; &quot;, &quot;_&quot;).ljust(50)
                
                pdf.cell(50, y, txt = line, border = 0,  new_x='RIGHT' , align = 'L', fill = False)

                
                line_seq = list(str(record.seq[cur_char : (cur_char + show_num)]))
                
                line_seq_highlight = list(str(record.letter_annotations['highlight'][cur_char : (cur_char + show_num)]))
                
                
                for i in range(len(line_seq)):
                    
                    if line_seq_highlight[i] == 'N' :
                        
                        filling = False
                    
                    if line_seq_highlight[i] == 'Y' :
                        
                        filling = True
                        
                    else:
                        
                        filling = False
                        
                    pdf.cell(x, y, txt = line_seq[i], border = 0,  new_x= 'RIGHT' ,  align = 'C', fill = filling)
                    
                pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)
                
                # now we need to print out the star info, if we've got it
                
                if star_info and rec_cnt == len(alignment):
                    
                    pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)
                    
                    rec_cnt = 0
                    
                    # print(star_info)
                    
                    line = (' ' * 50)
                    
                    pdf.cell(50, y, txt = line, border = 0,  new_x='RIGHT' , align = 'L', fill = False)
                    
                    star_info_seq = list(str(star_info[cur_char : (cur_char + show_num)]))

                    for i in range(len(star_info_seq)):
                        
                        pdf.cell(x, y, txt = star_info_seq[i], border = 0,  new_x= 'RIGHT' ,  align = 'C', fill = False)


                    
                pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)
                
            
                
            cur_char += show_num  
            
            pdf.cell(x, 3*y, txt = ' ' , border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot; ,  align = 'L', fill = False)


        
        pdf.output(&quot;out.pdf&quot;) 


write_alignment(alignment)


</code></pre>
<p>I tested it, with the <code>Multi.txt</code> Clustal alignment from above, output is</p>
<p><code>out.pdf</code> :</p>
<p><a href="https://i.sstatic.net/h6sMB.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/h6sMB.png" alt="enter image description here" /></a></p>
<p>.</p>
<p>One last note:</p>
<p>needed to replace <code>-</code> with <code>Z</code> in the Consensus string to be able to get reliable results from the RegEX line : <code>found = [m.start() for m in re.finditer( '(?='+re.escape(word_to_find)+')' , re.escape(str(rec.seq.replace('-', 'Z'))))]</code> , not sure what is going on here, got the snippet from a SO post.</p>

CC BY-SA 4.0
2022-12-24T00:33:59.087Z
2022-12-24T00:47:27.317Z
2022-12-24T00:47:27.317Z
pippo1980
<p>Ok, figured out a way, not sure its the best one,</p>
<p>nedd to install <code>fpdf2</code> (<code>pip install fpdf2</code>)</p>
<pre><code>from io import StringIO

from Bio import AlignIO  # Biopython 1.80

from fpdf import FPDF # pip install fpdf2
 
alignment = AlignIO.read(&quot;Multi.txt&quot;, &quot;clustal&quot;)


stri = StringIO()
    
AlignIO.write(alignment, stri, 'clustal' )

# print(stri.getvalue())

stri_lines = [ i for i in stri.getvalue().split('\n')]

# print(stri_lines)

pdf = FPDF(orientation=&quot;P&quot;, unit=&quot;mm&quot;, format=&quot;A4&quot;)
 
# Add a page
pdf.add_page()

pdf.add_font('FreeMono', '', 'FreeMono.ttf')

pdf.set_font(&quot;FreeMono&quot;, size = 8)

for x in stri_lines:
    pdf.cell(0, 5, txt = x, border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot;,  align = 'L', fill = False)
    
    # print(len(x))

pdf.output(&quot;out.pdf&quot;) 
</code></pre>
<p>output pdf  <code>out.pdf</code> :</p>
<p><a href="https://i.sstatic.net/oDg7V.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/oDg7V.png" alt="enter image description here" /></a></p>
<p>Not sure why the file Header is changed, think is something within Biopythion (!!! ???) you can check adding:</p>
<pre><code>with open('file_output.txt', 'w') as filehandler:
    AlignIO.write(alignment, filehandler, 'clustal')
</code></pre>
<p>I had to place a tff FreeMono font (Mono spaced font in my script directory) see: <code>pdf.add_font('FreeMono', '', 'FreeMono.ttf')</code> otherwise the alignement won't be printed in the correct way
<a href="https://graphicdesign.stackexchange.com/questions/13148/which-fonts-have-the-same-width-for-every-character">Which fonts have the same width for every character?</a>.</p>
<p>Attached a png of my pdf. See that you can highlight it,</p>
<p>using:</p>
<pre><code>pdf.set_fill_color(255, 255, 0)

filling = False
for x in stri_lines:
    if 'Consensus' in x:
        filling = True
    else:
        filling = False
    pdf.cell(0, 5, txt = x, border = 0,  new_x=&quot;LMARGIN&quot; , new_y=&quot;NEXT&quot;,  align = 'L', fill = filling)
</code></pre>
<p>or something similar you can highlight while printing:</p>
<p><a href="https://i.sstatic.net/9Dlzn.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/9Dlzn.png" alt="enter image description here" /></a></p>

CC BY-SA 4.0
2022-12-20T11:10:16.550Z
2022-12-20T21:57:50.623Z
2022-12-20T21:57:50.623Z
pippo1980
<p><em>This question was also asked on <a href="https://stackoverflow.com/questions/74852238/read-clustal-file-in-python">StackOverflow</a></em></p>
<p>I have a multiple sequence alignment (MSA) file derived from mafft in clustal format which I want to import into Python and save into a PDF file. I need to import the file and then highlight some specific words. I've tried to simply import the pdf of the MSA but after the highlight command doesn't work.</p>
<p>I need to print the file like this:
<a href="https://i.sstatic.net/OKNW2.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/OKNW2.png" alt="enter image description here" /></a></p>
<p><code>Multi.txt</code> :</p>
<pre><code>CLUSTAL format alignment by MAFFT FFT-NS-i (v7.453)


Consensus       --------------------------------------acgttttcgatatttatgccat
AMP             tttatattttctcctttttatgatggaacaagtctgcgacgttttcgatatttatgccat
                                                      **********************

Consensus       atgtgcatgttgtaaggttgaaagcaaaaatgaggggaaaaaaaatgaggtttttaataa
AMP             atgtgcatgttgtaaggttgaaagcaaaaatgaggggaaaaaaaatgaggtttttaataa
                ************************************************************

Consensus       ctacacatttagaggtctaggaaataaaggagtattaccatggaaatgtatttccctaga
AMP             ctacacatttagaggtctaggaaataaaggagtattaccatggaaatgtaattccctaga
                ************************************************** *********

Consensus       tatgaaatattttcgtgcagttacaacatatgtgaatgaatcaaaatatgaaaaattgaa
AMP             tatgaaatattttcgtgcagttacaacatatgtgaatgaatcaaaatatgaaaaattgaa
                ************************************************************

Consensus       atataagagatgtaaatatttaaacaaagaaactgtggataatgtaaatgatatgcctaa
AMP             atataagagatgtaaatatttaaacaaagaaactgtggataatgtaaatgatatgcctaa
                ************************************************************

Consensus       ttctaaaaaattacaaaatgttgtagttatgggaagaacaaactgggaaagcattccaaa
AMP             ttctaaaaaattacaaaatgttgtagttatgggaagaacaaactgggaaagcattccaaa
                ************************************************************

Consensus       aaaatttaaacctttaagcaataggataaatgttatattgtctagaaccttaaaaaaaga
AMP             aaaatttaaacctttaagcaataggataaatgttatattgtctagaaccttaaaaaaaga
                ************************************************************

Consensus       agattttgatgaagatgtttatatcattaacaaagttgaagatctaatagttttacttgg
AMP             agattttgatgaagatgtttatatcattaacaaagttgaagatctaatagttttacttgg
                ************************************************************

Consensus       gaaattaaattactataaatgttttattataggaggttccgttgtttatcaagaattttt
AMP             gaaattaaattactataaatgttttattataggaggttccgttgtttatcaagaattttt
                ************************************************************

Consensus       agaaaagaaattaataaaaaaaatatattttactagaataaatagtacatatgaatgtga
AMP             agaaaagaaattaataaaaaaaatatattttactagaataaatagtacatatgaatgtga
                ************************************************************

Consensus       tgtattttttccagaaataaatgaaaatgagtatcaaattatttctgttagcgatgtata
AMP             tgtattttttccagaaataaatgaaaatgagtatcaaattatttctgttagcgatgtata
                ************************************************************

Consensus       tactagtaacaatacaacattgga----------------------------------
AMP             tactagtaacaatacaacattggattttatcatttataagaaaacgaataataaaatg
                ************************                                  
</code></pre>
<p>How can I import the alignment and print in the new PDF with the right alignment of the sequences.</p>
<p>Thanks</p>

CC BY-SA 4.0
2022-12-20T08:58:01.223Z
Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking.
Community
CC BY-SA 4.0
2022-12-20T09:01:46.460Z
What words do you need to highlight?
M__
<python><phylogenetics><multiple-sequence-alignment>
Read Clustal file in Python
# Read Clustal file in Python

(Asked by: username_0 on 2022-12-20)

This question was also asked on StackOverflow
I have a multiple sequence alignment (MSA) file derived from mafft in clustal format which I want to import into Python and save into a PDF file. I need to import the file and then highlight some specific words. I've tried to simply import the pdf of the MSA but after the highlight command doesn't work.
I need to print the file like this:

Multi.txt :
CLUSTAL format alignment by MAFFT FFT-NS-i (v7.453)


Consensus       --------------------------------------acgttttcgatatttatgccat
AMP             tttatattttctcctttttatgatggaacaagtctgcgacgttttcgatatttatgccat
                                                      **********************

Consensus       atgtgcatgttgtaaggttgaaagcaaaaatgaggggaaaaaaaatgaggtttttaataa
AMP             atgtgcatgttgtaaggttgaaagcaaaaatgaggggaaaaaaaatgaggtttttaataa
                ************************************************************

Consensus       ctacacatttagaggtctaggaaataaaggagtattaccatggaaatgtatttccctaga
AMP             ctacacatttagaggtctaggaaataaaggagtattaccatggaaatgtaattccctaga
                ************************************************** *********

Consensus       tatgaaatattttcgtgcagttacaacatatgtgaatgaatcaaaatatgaaaaattgaa
AMP             tatgaaatattttcgtgcagttacaacatatgtgaatgaatcaaaatatgaaaaattgaa
                ************************************************************

Consensus       atataagagatgtaaatatttaaacaaagaaactgtggataatgtaaatgatatgcctaa
AMP             atataagagatgtaaatatttaaacaaagaaactgtggataatgtaaatgatatgcctaa
                ************************************************************

Consensus       ttctaaaaaattacaaaatgttgtagttatgggaagaacaaactgggaaagcattccaaa
AMP             ttctaaaaaattacaaaatgttgtagttatgggaagaacaaactgggaaagcattccaaa
                ************************************************************

Consensus       aaaatttaaacctttaagcaataggataaatgttatattgtctagaaccttaaaaaaaga
AMP             aaaatttaaacctttaagcaataggataaatgttatattgtctagaaccttaaaaaaaga
                ************************************************************

Consensus       agattttgatgaagatgtttatatcattaacaaagttgaagatctaatagttttacttgg
AMP             agattttgatgaagatgtttatatcattaacaaagttgaagatctaatagttttacttgg
                ************************************************************

Consensus       gaaattaaattactataaatgttttattataggaggttccgttgtttatcaagaattttt
AMP             gaaattaaattactataaatgttttattataggaggttccgttgtttatcaagaattttt
                ************************************************************

Consensus       agaaaagaaattaataaaaaaaatatattttactagaataaatagtacatatgaatgtga
AMP             agaaaagaaattaataaaaaaaatatattttactagaataaatagtacatatgaatgtga
                ************************************************************

Consensus       tgtattttttccagaaataaatgaaaatgagtatcaaattatttctgttagcgatgtata
AMP             tgtattttttccagaaataaatgaaaatgagtatcaaattatttctgttagcgatgtata
                ************************************************************

Consensus       tactagtaacaatacaacattgga----------------------------------
AMP             tactagtaacaatacaacattggattttatcatttataagaaaacgaataataaaatg
                ************************                                  

How can I import the alignment and print in the new PDF with the right alignment of the sequences.
Thanks

**Tags:** python, phylogenetics, multiple-sequence-alignment
> **Comments:**
> **username_1**: Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking.  
> **username_2**: What words do you need to highlight?  

---

## 3 answers

### Answer by username_4 on 2022-12-20. Score: 2 | Accepted answer

Ok, figured out a way, not sure its the best one,
nedd to install fpdf2 (pip install fpdf2)
from io import StringIO

from Bio import AlignIO  # Biopython 1.80

from fpdf import FPDF # pip install fpdf2
 
alignment = AlignIO.read("Multi.txt", "clustal")


stri = StringIO()
    
AlignIO.write(alignment, stri, 'clustal' )

# print(stri.getvalue())

stri_lines = [ i for i in stri.getvalue().split('\n')]

# print(stri_lines)

pdf = FPDF(orientation="P", unit="mm", format="A4")
 
# Add a page
pdf.add_page()

pdf.add_font('FreeMono', '', 'FreeMono.ttf')

pdf.set_font("FreeMono", size = 8)

for x in stri_lines:
    pdf.cell(0, 5, txt = x, border = 0,  new_x="LMARGIN" , new_y="NEXT",  align = 'L', fill = False)
    
    # print(len(x))

pdf.output("out.pdf") 

output pdf  out.pdf :

Not sure why the file Header is changed, think is something within Biopythion (!!! ???) you can check adding:
with open('file_output.txt', 'w') as filehandler:
    AlignIO.write(alignment, filehandler, 'clustal')

I had to place a tff FreeMono font (Mono spaced font in my script directory) see: pdf.add_font('FreeMono', '', 'FreeMono.ttf') otherwise the alignement won't be printed in the correct way
Which fonts have the same width for every character?.
Attached a png of my pdf. See that you can highlight it,
using:
pdf.set_fill_color(255, 255, 0)

filling = False
for x in stri_lines:
    if 'Consensus' in x:
        filling = True
    else:
        filling = False
    pdf.cell(0, 5, txt = x, border = 0,  new_x="LMARGIN" , new_y="NEXT",  align = 'L', fill = filling)

or something similar you can highlight while printing:


---

### Answer by username_3 on 2022-12-21. Score: 3

pyMSAviz may help with this problem.
pyMSAviz is a Python tool with a convenient CLI/API implementation for visualizing MSA files.
For example, you can output MSA figure in PDF format with the following command.
pymsaviz -i Multi.txt -o msa_result.pdf --format clustal \
         --wrap_length 60 --show_count --color_scheme Identity


You can also use pyMSAviz's API to configure the appearance of the figure in more detail, such as highlighting.

> **Comments:**
> **username_2**: Thanks for your solution Moshi. I personally think @pippo1980's solution is cooler because they leverage AlignIO within the StringIO (or StringIO within AlignIO). This permits a lot of flexibility in possible solutions because we use BioPython (AlignIO) all the time and is very cool OO.  
> **username_4**: personally I think your tool is supercool, spent last 3 days googling for such thing, but wasnt able to find anything ( I mean espript3 is supersupercool like jalview but no python API ... could be wrong). Wasted a day trying to understand how to get PyMuPDF  to find words, like 'ggaag' in my out.pdf with no luck. I am intrigued by your tool. Is there a way to color conserved residues all blue and non conserved like red ?? Was thinking about how to realize such a thing, Pyfpdf2 can highligh single cell and I should find a way to print each single alignment letter in a single cell and devising  
> **username_4**: a strategy to do that. Sorry I cannot upvote more than once.  
> **username_4**: yep found  --color_scheme Identity in your examples !!!  
> **username_2**: Hi @pippo1980. I'm pretty sure you could get yours working. Note that MEGA X alignment editor will colour conserved residues (but you don't get to choose the colour) and can't do non-conserved residues. Merry Christmas!  

---

### Answer by username_4 on 2022-12-24. Score: 2

adding, a second answer because the OP request about

I need to import the file and then highlight some specific word

was making me uneasy. I kept using the Pyfpdf2 library and made use of the Biopython SeqRecord.letter_annotations attribute to track Nucleotides index composing the words found by RegEx. Tried to use the page.search_for() of the PyMuPDF library directly on the output pdf of my previous answer but was not getting word accross line breaks [if anybody know better, please help].
Here my code, it's a two half problem, first find the word (in my example is 'atat'), then write everything to pdf, I stole it from from /master/Bio/AlignIO/ClustalIO.py/   def write_alignment(self, alignment): from class ClustalWriter(SequentialAlignmentWriter) with some modding [I believe but not absolutely sure that ...A cell by definition is uniform in font and color... ]:
import re

from Bio import AlignIO  # Biopython 1.80

from fpdf import FPDF # pip install fpdf2
 

alignment = AlignIO.read("Multi.txt", "clustal")


word_to_find = 'atat'

for rec in alignment:
    
    rec.letter_annotations['highlight'] = 'N'*len(rec.seq)

# for rec in alignment:    
#     print('rec.id : ', rec.id, 'highlight : ', rec.letter_annotations['highlight'])
    
    
for rec in alignment:
    
    # print(str(rec.seq))
    
    found = [m.start() for m in re.finditer( '(?='+re.escape(word_to_find)+')' , re.escape(str(rec.seq.replace('-', 'Z'))))]
    
    # print(rec.id , ' found : ', found)
    
    for i in found :
        
            a = list(rec.letter_annotations['highlight'])
    
            a[i:i+len(word_to_find)] = 'Y'*len(word_to_find)
            
            rec.letter_annotations['highlight'] = ''.join(i for i in a)
            
            # print(rec.letter_annotations['highlight'])
            
            

max_length = alignment.get_alignment_length()


# stolen from /master/Bio/AlignIO/ClustalIO.py/   def write_alignment(self, alignment): from class ClustalWriter(SequentialAlignmentWriter):
def write_alignment(alignment):
    
        x = 2
        
        y = 2

        pdf = FPDF(orientation="P", unit="mm", format="A4")
         
        # Add a page
        pdf.add_page()

        pdf.add_font('FreeMono', '', 'FreeMono.ttf')

        pdf.set_font("FreeMono", size = 12)
        
        pdf.set_fill_color(255, 255, 0)

    
        """Use this to write (another) single alignment to an open file."""
        if len(alignment) == 0:
            raise ValueError("Must have at least one sequence")
        if alignment.get_alignment_length() == 0:
            # This doubles as a check for an alignment object
            raise ValueError("Non-empty sequences are required")

        # Old versions of the parser in Bio.Clustalw used a ._version property
        try:
            version = str(alignment._version)
        except AttributeError:
            version = ""
        if not version:
            version = "1.81"
        if version.startswith("2."):
            # e.g. 2.0.x
            output = f"CLUSTAL {version} multiple sequence alignment"
        else:
            # e.g. 1.81 or 1.83
            output = f"CLUSTAL X ({version}) multiple sequence alignment"
            
        pdf.cell(0, y, txt = output , border = 0,  new_x='LMARGIN'  , new_y="NEXT" ,  align = 'L', fill = False)
        pdf.cell(0, 4*y, txt = '' , border = 0,  new_x='LMARGIN'  , new_y="NEXT" ,  align = 'L', fill = False)
        

        cur_char = 0
        # # max_length = len(alignment[0])
        max_length = alignment.get_alignment_length()


        if max_length <= 0:
            raise ValueError("Non-empty sequences are required")

        if "clustal_consensus" in alignment.column_annotations:
            star_info = alignment.column_annotations["clustal_consensus"]
        else:
            try:
                # This was originally stored by Bio.Clustalw as ._star_info
                star_info = alignment._star_info
            except AttributeError:
                star_info = None

        # keep displaying sequences until we reach the end
        
        rec_cnt = 0
        
        while cur_char != max_length:
            # calculate the number of sequences to show, which will
            # be less if we are at the end of the sequence
            if (cur_char + 50) > max_length:
                show_num = max_length - cur_char
            else:
                show_num = 50

            # go through all of the records and print out the sequences
            # when we output, we do a nice 80 column output, although this
            # may result in truncation of the ids.
            for record in alignment:
                
                rec_cnt += 1 
                
                # Make sure we don't get any spaces in the record
                # identifier when output in the file by replacing
                # them with underscores:
                line = record.id[0:30].replace(" ", "_").ljust(50)
                
                pdf.cell(50, y, txt = line, border = 0,  new_x='RIGHT' , align = 'L', fill = False)

                
                line_seq = list(str(record.seq[cur_char : (cur_char + show_num)]))
                
                line_seq_highlight = list(str(record.letter_annotations['highlight'][cur_char : (cur_char + show_num)]))
                
                
                for i in range(len(line_seq)):
                    
                    if line_seq_highlight[i] == 'N' :
                        
                        filling = False
                    
                    if line_seq_highlight[i] == 'Y' :
                        
                        filling = True
                        
                    else:
                        
                        filling = False
                        
                    pdf.cell(x, y, txt = line_seq[i], border = 0,  new_x= 'RIGHT' ,  align = 'C', fill = filling)
                    
                pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x="LMARGIN" , new_y="NEXT" ,  align = 'L', fill = False)
                
                # now we need to print out the star info, if we've got it
                
                if star_info and rec_cnt == len(alignment):
                    
                    pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x="LMARGIN" , new_y="NEXT" ,  align = 'L', fill = False)
                    
                    rec_cnt = 0
                    
                    # print(star_info)
                    
                    line = (' ' * 50)
                    
                    pdf.cell(50, y, txt = line, border = 0,  new_x='RIGHT' , align = 'L', fill = False)
                    
                    star_info_seq = list(str(star_info[cur_char : (cur_char + show_num)]))

                    for i in range(len(star_info_seq)):
                        
                        pdf.cell(x, y, txt = star_info_seq[i], border = 0,  new_x= 'RIGHT' ,  align = 'C', fill = False)


                    
                pdf.cell(x, 2*y, txt = ' ' , border = 0,  new_x="LMARGIN" , new_y="NEXT" ,  align = 'L', fill = False)
                
            
                
            cur_char += show_num  
            
            pdf.cell(x, 3*y, txt = ' ' , border = 0,  new_x="LMARGIN" , new_y="NEXT" ,  align = 'L', fill = False)


        
        pdf.output("out.pdf") 


write_alignment(alignment)



I tested it, with the Multi.txt Clustal alignment from above, output is
out.pdf :

.
One last note:
needed to replace - with Z in the Consensus string to be able to get reliable results from the RegEX line : found = [m.start() for m in re.finditer( '(?='+re.escape(word_to_find)+')' , re.escape(str(rec.seq.replace('-', 'Z'))))] , not sure what is going on here, got the snippet from a SO post.


---

