Python avec DOCX

Résolu
Steph -  
mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   -

Bonjour,

Je suis un nouvel utilisateur de Python.

J'utilise python 3.11 avec python-docx-0.8.11 avec lequel je crée un document WORD contenant un tableau de 2 colonnes et X lignes.

Je voudrais pouvoir utiliser la propriété du tableau "Autoriser le fractionnement des lignes sur plusieurs pages" afin de ne pas avoir de ligne sur plusieurs pages.

Est-ce que quelqu'un sait comment accéder à cette propriété ?

Merci

Stéphane


Windows / Firefox 102.0

3 réponses

  1. mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   7 940
     

    Bonjour Stéphane,

    Quelques questions préalables pour mieux cerner ton problème et ce que tu cherches à faire.

    • Pourquoi stocker un tableau dans un fichier docx, ce n'est pas plutôt un fichier excel que tu devrais créer ? Si oui, on utilises généralement pandas et on crée une dataframe, et on la sauve au format excel (voir ici).
    • Il semble exister une version plus récente de python-docx, pourquoi ne pas l'utiliser ?
    • As-tu regardé la documentation de python-docx ? En particulier l'API de l'objet Table et l'index ?
    • As-tu commencé à écrire un script python ? Si oui que contient-il ?

    Bonne chance

    0
  2. Steph
     

    Bonjour,

    J'ai énormément d'images à importer dans un rapport de type fichier word.

    Pour ce faire j'utilise une hiérarchie de répertoires dans lesquels je place mes images à importer. Mon développement Python scrute les répertoires et crée le fichier word en prenant chaque nom de répertoire et en lui attribuant un style de Titre avec le niveau hiérarchique correspondant et importe toutes les images s'y trouvant dans un tableau de 2 colonnes (en modifiant les dpi et les tailles).

    Je suis sur un réseau totalement déconnecté du WEB, c'est pour ça que j'utilise la version 0.8.11 de docx. Je ne pense pas que cela vienne de la version, c'est juste que je ne sais pas bien me servir de python.

    J'ai regardé la documentation mais je bloque avec la fonction identifiée à cette Page (property : AllowBreakAcrossPages)

    Voici mon code actuel sans l'interface utilisateur :

    Ma tentative se situe à la ligne 110.

    # -*- coding: utf8 -*-
    """
    Fichier script et fonctionnel pour l'écriture des documents docx
    """
    from docx import Document
    from docx.shared import Inches
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.enum.table import WD_TABLE_ALIGNMENT
    
    import PIL
    from PIL import Image
    import os
    import io
    import time
    import interface_pour_docx as itf
    
    #Formats supportés par Pillow : PNG,JPEG,PPM,TIFF,BMP
    
    
    def process_image(image_file,image_loc,table_for_dir_count,cell,dpi=220,inches=3,titres=True):
        """
        Fonction permettant le traitement d'un fichier image rencontré dans un dossier
    
        Arguments (* pour les arguments obligatoires):
            *image_file : image au format tel qu'ouvert par Pillow
            *image_loc : emplacement de l'image
            *table_for_dir_count : compteur du nombre d'images déjà traitées pour la partie ou la sous-partie
            *cell : cellule dans laquelle ajouter l'image
            dpi : résolution en points par pouce
            inches : taille de l'image dans le docx (en pouces)
            titres : variable indiquant si l'on ajoute ou non les titres des images en tant que légende
    
        Renvoie :
            Le compteur du nombre d'images déjà traitées incrémenté
        """
    
        # On récupère les informations de dpi et on recadre l'image selon le dpi objectif
        (w,h) = image_file.size
        w_obj = inches*dpi
        h_obj = h*w_obj/w
        
        image_out_shape = (int(w_obj),int(h_obj))
        resized_img = image_file.resize(image_out_shape)
    
        #Sauvegarde de l'image sous forme de stream
        imdata = io.BytesIO()
        resized_img.save(imdata,format='png')
    
        # On ajoute l'image à la cellule, ainsi que son titre
        p = cell.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run()
        run.add_picture(imdata,width=Inches(inches))
        if titres:
            p.add_run(image_loc.split(".")[0].split("/")[-1])
        else :
            p.add_run(" ")
        # Enfin, on renvoie le compteur du nombre d'images traitées incrémenté
        table_for_dir_count+=1
        del imdata
        return table_for_dir_count
    
    
    def explore_dir(document,directory,depth,f_part_count = "",dpi=220,avec_numeros=True,titres=True,niveau_initial=0):
        """
        Fonction permettant l'exploration d'un dossier
    
        Arguments (* pour les arguments obligatoires):
            *document : le document de sortie (docx.Document)
            *directory : le répertoire à traiter
            *depth : la profondeur du titre
            f_part_count : un préfixe éventuel à rajouter pour le titre de la partie (pour l'affichage console uniquement)
            dpi : résolution des images en points par pouce
            avec_numeros : indique si les titres des sous dossiers ont des numéros (ex : 01 dossier-1, 02 dossier-2...)
            titres : variable indiquant si l'on ajoute ou non les titres des images en tant que légende
            niveau_initial : variable indiquant le niveau initial de traitement
    
        Renvoie :
            ne renvoie rien
        """
        print(f"traitement de la partie {f_part_count} (dossier {directory})")
        # De façon récursive, on explore les différents dossiers et sous dossiers, en ajoutant le titre en tant que titre de partie/sous-partie, et en ajoutant les fichiers
        if depth != niveau_initial and avec_numeros:
            header = " ".join(directory.split("/")[-1].split(" ")[1:])
        else:
            header = directory.split("/")[-1]
        heading = document.add_heading(header,level=depth)
        table_for_dir_count = 0 # Compteur du nombre d'images déjà ajoutées dans cette partie
        sub_fold_counter = 0
        lst = os.listdir(directory)
        lst.sort()
        for element in lst:
            # On liste tout ce qui se trouve dans le répertoire dans l'ordre alphabétique
            if os.path.isdir(directory+"/"+element):
                sub_fold_counter += 1
                if depth==0:
                    new_f_part_count = f"{sub_fold_counter}."
                else:
                    new_f_part_count = f_part_count+f"{sub_fold_counter}."
                # Si l'élément est un répertoire, on fouille dedans de façon récursive
                explore_dir(document,directory+"/"+element,depth=depth+1,f_part_count=new_f_part_count,dpi=dpi,avec_numeros=avec_numeros,titres=titres,niveau_initial=niveau_initial)
            else:
                # Sinon, on teste de l'ouvrir en tant qu'image, et si ça marche on la traîte
                try:
                    img = Image.open(directory+"/"+element)
                    # Selon le nombre d'images déjà présentes, on ajoute ou non une colonne, et on pointe vbers la première cellule vide
                    if table_for_dir_count==0:
                        table = document.add_table(rows=1,cols=2,style="Table Grid")
                        
                        #table.rows.AllowBreakAcrossPages=False
                          
                        cell = table.rows[0].cells[1]
                        cell.width=Inches(3.15)
                        cell = table.rows[0].cells[0]
                        table.alignment = WD_TABLE_ALIGNMENT.CENTER
                        cell.width=Inches(3.15)
                    elif table_for_dir_count%2==0:
                        cell = table.add_row().cells[0]
                    else:
                        cell = table.rows[-1].cells[1]
                    table_for_dir_count = process_image(img,directory+"/"+element,table_for_dir_count,cell,dpi=dpi,titres=titres)
                    
                except PIL.UnidentifiedImageError:
                    continue
    
    
    def main():
        """
        Fonction principale : on ouvre la fenêtre pour récupérer les options du traitement, on crée le document et on lance le script d'exploration.
        Il ne reste plus qu'à sauvegarder et voilà !
        """
        name,dpi,in_directory,out_directory,dossiers_avec_numeros,titres,niveau_initial = itf.fenetre_analyse_docx()
    
        # Création du document
        document = Document()
    
        #Exploration
        explore_dir(document,in_directory,niveau_initial,dpi=dpi,avec_numeros=dossiers_avec_numeros,titres=titres,niveau_initial=niveau_initial)
    
        # Sauvegarde
        print("Sauvegarde du document")
        document.save(out_directory+f"/{name}.docx")
        print("Terminé !")
        time.sleep(1.)
    
    
    if __name__=="__main__":
        main()

    J'espère avoir été suffisamment clair.

    Merci de l'attention accordée

    Stéphane

    0
  3. mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   7 940
     

    Bonjour,

    Je pense avoir compris. Le seul soucis c'est que je ne connais pas docx et donc j'aurais tendance à t'inspirer de cette solution.

    Ceci dit, je n'aurais pas procédé comme toi. En effet, tu pourrais te contenter de créer un fichier HTML, bien plus simple à générer, et y inclure tes images.

    En admettant que cette solution t'intéresse, je te propose donc cette autre solution, tu n'as plus besoin ni de docx, ni de PIL. Le document s'affichera de manière "responsive" aux dimensions de ton navigateur, et tu n'auras plus de soucis liés aux pages inhérentes aux documents word. Enfin, tu pourras facilement imprimer ton document depuis ton navigateur.

    #!/usr/bin/env python3
    
    from pathlib import Path
    
    def make_html(
        photos_dir: Path,
        output_filename: Path,
        num_columns: int = 2,
        max_files_per_dir: int = None
    ):
        with open(output_filename, "w") as f:
            subdirs = photos_dir.glob("**/")
            print(
                f"""
            <html>
                <head>
                    <style>
                        h1 {{
                            color: blue;
                        }}
                        h2 {{
                            color: darkgreen;
                        }}
                        h3 {{
                            color: orange;
                        }}
                        h4 {{
                            color: red;
                        }}
                        h5 {{
                            color: purple;
                        }}
                        .grid-container {{
                            display: grid;
                            grid-template-columns: {"auto " *  num_columns};
                            padding: 2%;
                            width: 100%;
                        }}
                        .grid-item {{
                            padding: 5%;
                        }}
                    </style>
                </head>
                <body>""",
                file=f
            )
            for subdir in subdirs:
                n = len(str(subdir).split("/"))
                print(
                    f"""
                    <h{n}><pre>{subdir}</pre></h{n}>
                    <div class="grid-container">""",
                    file=f
                )
                photos = subdir.glob("**/*jpg")
                for photo in list(photos)[:max_files_per_dir]:
                    print(
                        f"""
                            <div class="grid-item">
                                <pre>{photo.name}</pre>
                                <img src="{photo}" width="100%"/>
                            </div>""",
                        file=f
                    )
                print("""
                    </div>""",
                    file=f
                )
            print(
                """
                </body>
            </html>""",
                file=f
            )
    
    def main():
        photos_dir = Path.home() / "Photos"
        output_filename = "toto.html"
        num_columns = 2
        max_files_per_dir = 10 # None --si tu veux prendre tous les fichiers de chaque dossier
        make_html(photos_dir, output_filename, num_columns)
    
    main()
    

    Pour améliorer ce programme, tu peux utiliser le module argparse afin que ton exécutable prenne des paramètres, ce qui évitera de les mettre en dur dans la fonction main.

    Quelques remarques par rapport à ce programme :

    • L'énorme avantage de HTML/CSS comparé à DOCX, c'est qu'il est portable.
      • Dans le cas général on n'a pas forcément word ou libreoffice installé sur son ordinateur/tablette/portable, mais on a toujours un navigateur.
    • Ici, mon fichier HTML stocke des références vers les images.
      • L'avantage, c'est que le fichier HTML est très petit, puisque les images ne sont pas stockées dedans.
      • En contrepartie, le rendu ne peut se faire qu'en présence de cette hiérarchie d'image.
      • Si tu veux générer un fichier "standalone", une solution consisterait à encoder chaque image en base64 (voir ici).
    • En CSS, la bonne manière de faire pour afficher les image est d'utiliser une grille.
      • L'avantage, c'est qu'on n'a pas besoin de se préoccuper des éventuelles cases vides à ajouter pour compléter la dernière ligne du tableau.
      • Le principe est simple. On définit une grille, on énumère les éléments sans se préoccuper de leur nombre, et le navigateur se débrouille pour les placer et les redimensionner. Le nombre de colonne de ladite grille est défini dans le style de grid-container (un "auto" par colonne). Pour plus de détails, voir ici.
    • Mon code python utilise des f-strings. Celles-ci sont standard depuis python 3.6 et allègent considérablement le code.
      • Cela permet d'injecter du code (entre accolades) au moment de créer la chaîne. Par exemple, je génère dans le style CSS autant de "auto" que la valeur définie dans num_columns.
      • En contrepartie, il faut échapper les accolades en les doublant.
    • Afin de simplifier les manipulations sur les chemins, j'utilise pathlib qui est un module standard dans les versions modernes de python3.
      • Ma recommandation est d'utiliser autant que possible pathlib en python quand tu manipules des chemins, ça permet d'avoir un code portable entre windows et Linux et souvent plus élégant.
    • Si tu veux supporter que le programme considère plusieurs extensions de fichiers (par exemple ".jpg", ".png", etc), il faut adapter légèrement le programme en s'inspirant de ce message.

    Bonne chance

    0
    1. Steph
       

      Bonjour,

      Merci beaucoup pour toutes ces explications, mais j'ai vraiment besoin d'activer la propriété "AllowBreakAcrossPages" de toutes mes tables contenues dans le fichier Word généré par mon petit développement.

      La génération de ce document avec les styles utilisés viens s'insérer dans un autre document word (copié collé) qui est en fait un rapport obligatoirement au format Word.

      J'ai le nom de la propriété de la table dans Word mais je ne sais pas la renseigner en Python.

      Merci encore pour tes infos, je vais continuer à creuser mais c'est difficile pour un néophyte :)

      Stéphane

      0
      1. mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   7 940 > Steph
         

        Ok, du coup si le format Word est imposé, ma solution tombe effectivement à l'eau. Du coup, avec python-docx, as-tu essayé ceci ?

        Bonne chance

        0
      2. Steph > mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention  
         

        Après plusieurs tests, j'ai abandonné la piste XML via Python-docx.

        J'ai solutionné mon problème en modifiant le style des tableaux dans le fichier Word, ce qui a eu pour effet de modifier tous les tableaux en rendant les lignes non fractionnables.

        Objectif atteint de manière détournée car je ne suis pas informaticien.

        Merci pour votre participation.

        Stéphane

        1
      3. mamiemando Messages postés 33228 Date d'inscription   Statut Modérateur Dernière intervention   7 940 > Steph
         

        Merci d'avoir pris le temps de nous expliquer comment tu t'en étais sorti. Ton retour d'information aidera probablement les autres personnes confrontées à ce besoin et qui tomberont sur cette discussion. Bonne continuation

        0