Python avec DOCX

Résolu
Steph - Modifié le 18 déc. 2023 à 16:46
mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024 - 25 janv. 2024 à 10:48

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

A voir également:

3 réponses

mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024 7 775
18 déc. 2023 à 16:52

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

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
mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024 7 775
Modifié le 19 déc. 2023 à 18:11

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

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
mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024 7 775 > Steph
20 déc. 2023 à 11:41

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
Steph > mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024
10 janv. 2024 à 14:18

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
mamiemando Messages postés 33252 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 26 août 2024 7 775 > Steph
25 janv. 2024 à 10:48

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