From b985957814e318474eabb2ac1f1a2f589a83031a Mon Sep 17 00:00:00 2001 From: Maifee Ul Asad Date: Thu, 23 Dec 2021 20:40:44 +0600 Subject: [PATCH 1/5] image styling --- htmldocx/h2d.py | 53 +++++++++++++++++++++++++++++++++++++----------- tests/text1.html | 2 ++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/htmldocx/h2d.py b/htmldocx/h2d.py index 6a0e113..6b58c4f 100644 --- a/htmldocx/h2d.py +++ b/htmldocx/h2d.py @@ -12,6 +12,7 @@ How to deal with block level style applied over table elements? e.g. text align """ +import enum import re, argparse import io, os import urllib.request @@ -138,6 +139,21 @@ def delete_paragraph(paragraph): p.getparent().remove(p) p._p = p._element = None + +class ImageAlignment(enum.Enum): + LEFT = 1 + CENTER = 2 + RIGHT = 3 + + +def get_image_alignment(image_style): + if image_style == 'float: right;': + return ImageAlignment.RIGHT + if image_style == 'display: block; margin-left: auto; margin-right: auto;': + return ImageAlignment.CENTER + return ImageAlignment.LEFT + + font_styles = { 'b': 'bold', 'strong': 'bold', @@ -239,7 +255,7 @@ def add_styles_to_run(self, style): # TODO map colors to named colors (and extended colors...) # For now set color to black to prevent crashing self.run.font.color.rgb = RGBColor(*colors) - + if 'background-color' in style: if 'rgb' in style['background-color']: color = color = re.sub(r'[a-z()]+', '', style['background-color']) @@ -280,7 +296,7 @@ def handle_li(self): else: list_style = styles['LIST_BULLET'] - self.paragraph = self.doc.add_paragraph(style=list_style) + self.paragraph = self.doc.add_paragraph(style=list_style) self.paragraph.paragraph_format.left_indent = Inches(min(list_depth * LIST_INDENT, MAX_INDENT)) self.paragraph.paragraph_format.line_spacing = 1 @@ -320,7 +336,20 @@ def handle_img(self, current_attrs): else: # avoid exposing filepaths in document self.doc.add_paragraph("" % get_filename_from_url(src)) - # add styles? + ''' + #adding style + For right-alignment: `'float: right;'` + For center-alignment: `'display: block; margin-left: auto; margin-right: auto;'` + Everything else would be Left aligned + ''' + if 'style' in current_attrs: + style = current_attrs['style'] + image_alignment = get_image_alignment(style) + last_paragraph = self.doc.paragraphs[-1] + if image_alignment == ImageAlignment.RIGHT: + last_paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT + if image_alignment == ImageAlignment.CENTER: + last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER def handle_table(self): """ @@ -355,7 +384,7 @@ def handle_table(self): child_parser.add_html_to_cell(cell_html, docx_cell) cell_col += 1 cell_row += 1 - + # skip all tags until corresponding closing tag self.instances_to_skip = len(table_soup.find_all('table')) self.skip_tag = 'table' @@ -588,7 +617,7 @@ def get_tables(self): self.include_tables = False return # find other way to do it, or require this dependency? - self.tables = self.ignore_nested_tables(self.soup.find_all('table')) + self.tables = self.ignore_nested_tables(self.soup.find_all('table')) self.table_no = 0 def run_process(self, html): @@ -618,7 +647,7 @@ def add_html_to_cell(self, html, cell): # cells must end with a paragraph or will get message about corrupt file # https://stackoverflow.com/a/29287121 if not self.doc.paragraphs: - self.doc.add_paragraph('') + self.doc.add_paragraph('') def parse_html_file(self, filename_html, filename_docx=None): with open(filename_html, 'r') as infile: @@ -629,23 +658,23 @@ def parse_html_file(self, filename_html, filename_docx=None): path, filename = os.path.split(filename_html) filename_docx = '%s/new_docx_file_%s' % (path, filename) self.doc.save('%s.docx' % filename_docx) - + def parse_html_string(self, html): self.set_initial_attrs() self.run_process(html) return self.doc if __name__=='__main__': - + arg_parser = argparse.ArgumentParser(description='Convert .html file into .docx file with formatting') arg_parser.add_argument('filename_html', help='The .html file to be parsed') arg_parser.add_argument( - 'filename_docx', - nargs='?', - help='The name of the .docx file to be saved. Default new_docx_file_[filename_html]', + 'filename_docx', + nargs='?', + help='The name of the .docx file to be saved. Default new_docx_file_[filename_html]', default=None ) - arg_parser.add_argument('--bs', action='store_true', + arg_parser.add_argument('--bs', action='store_true', help='Attempt to fix html before parsing. Requires bs4. Default True') args = vars(arg_parser.parse_args()) diff --git a/tests/text1.html b/tests/text1.html index 71b8e2a..16e5a8f 100644 --- a/tests/text1.html +++ b/tests/text1.html @@ -14,6 +14,8 @@
2 + 3 = 5↵this is code

A picture from file:

A picture from url:

+

A centered picture from url:

+

A right aligned picture from url:

A picture from url that's broken:

heading 1

    From aa9aeec6a0e29a2c64f51141dc9d030eb50d5c4c Mon Sep 17 00:00:00 2001 From: Maifee Ul Asad Date: Thu, 23 Dec 2021 20:42:29 +0600 Subject: [PATCH 2/5] //reformat file --- htmldocx/h2d.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/htmldocx/h2d.py b/htmldocx/h2d.py index 6b58c4f..a9616ed 100644 --- a/htmldocx/h2d.py +++ b/htmldocx/h2d.py @@ -31,7 +31,7 @@ # values in inches INDENT = 0.25 LIST_INDENT = 0.5 -MAX_INDENT = 5.5 # To stop indents going off the page +MAX_INDENT = 5.5 # To stop indents going off the page # Style to use with tables. By default no style is used. DEFAULT_TABLE_STYLE = None @@ -43,6 +43,7 @@ def get_filename_from_url(url): return os.path.basename(urlparse(url).path) + def is_url(url): """ Not to be used for actually validating a url, but in our use case we only @@ -51,6 +52,7 @@ def is_url(url): parts = urlparse(url) return all([parts.scheme, parts.netloc, parts.path]) + def fetch_image(url): """ Attempts to fetch an image from a url. @@ -65,9 +67,11 @@ def fetch_image(url): except urllib.error.URLError: return None + def remove_last_occurence(ls, x): ls.pop(len(ls) - ls[::-1].index(x) - 1) + def remove_whitespace(string, leading=False, trailing=False): """Remove white space from a string. @@ -133,6 +137,7 @@ def remove_whitespace(string, leading=False, trailing=False): # TODO need some way to get rid of extra spaces in e.g. text text return re.sub(r'\s+', ' ', string) + def delete_paragraph(paragraph): # https://github.com/python-openxml/python-docx/issues/33#issuecomment-77661907 p = paragraph._element @@ -176,6 +181,7 @@ def get_image_alignment(image_style): 'LIST_NUMBER': 'List Number', } + class HtmlToDocx(HTMLParser): def __init__(self): @@ -204,9 +210,9 @@ def set_initial_attrs(self, document=None): self.doc = document else: self.doc = Document() - self.bs = self.options['fix-html'] # whether or not to clean with BeautifulSoup + self.bs = self.options['fix-html'] # whether or not to clean with BeautifulSoup self.document = self.doc - self.include_tables = True #TODO add this option back in? + self.include_tables = True # TODO add this option back in? self.include_images = self.options['images'] self.include_styles = self.options['styles'] self.paragraph = None @@ -249,7 +255,7 @@ def add_styles_to_run(self, style): colors = [int(x) for x in color.split(',')] elif '#' in style['color']: color = style['color'].lstrip('#') - colors = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) + colors = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) else: colors = [0, 0, 0] # TODO map colors to named colors (and extended colors...) @@ -262,12 +268,12 @@ def add_styles_to_run(self, style): colors = [int(x) for x in color.split(',')] elif '#' in style['background-color']: color = style['background-color'].lstrip('#') - colors = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) + colors = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) else: colors = [0, 0, 0] # TODO map colors to named colors (and extended colors...) # For now set color to black to prevent crashing - self.run.font.highlight_color = WD_COLOR.GRAY_25 #TODO: map colors + self.run.font.highlight_color = WD_COLOR.GRAY_25 # TODO: map colors def apply_paragraph_style(self, style=None): try: @@ -289,7 +295,7 @@ def handle_li(self): if list_depth: list_type = self.tags['list'][-1] else: - list_type = 'ul' # assign unordered if no tag + list_type = 'ul' # assign unordered if no tag if list_type == 'ol': list_style = styles['LIST_NUMBER'] @@ -404,7 +410,6 @@ def handle_link(self, href, text): hyperlink = docx.oxml.shared.OxmlElement('w:hyperlink') hyperlink.set(docx.oxml.shared.qn('r:id'), rel_id) - # Create sub-run subrun = self.paragraph.add_run() rPr = docx.oxml.shared.OxmlElement('w:rPr') @@ -446,7 +451,7 @@ def handle_starttag(self, tag, attrs): return elif tag == 'ol' or tag == 'ul': self.tags['list'].append(tag) - return # don't apply styles for now + return # don't apply styles for now elif tag == 'br': self.run.add_break() return @@ -468,14 +473,14 @@ def handle_starttag(self, tag, attrs): pPr = self.paragraph._p.get_or_add_pPr() pBdr = OxmlElement('w:pBdr') pPr.insert_element_before(pBdr, - 'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap', - 'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN', - 'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', 'w:spacing', 'w:ind', - 'w:contextualSpacing', 'w:mirrorIndents', 'w:suppressOverlap', 'w:jc', - 'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap', - 'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr', - 'w:pPrChange' - ) + 'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap', + 'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN', + 'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', 'w:spacing', 'w:ind', + 'w:contextualSpacing', 'w:mirrorIndents', 'w:suppressOverlap', 'w:jc', + 'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap', + 'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr', + 'w:pPrChange' + ) bottom = OxmlElement('w:bottom') bottom.set(qn('w:val'), 'single') bottom.set(qn('w:sz'), '6') @@ -664,8 +669,8 @@ def parse_html_string(self, html): self.run_process(html) return self.doc -if __name__=='__main__': +if __name__ == '__main__': arg_parser = argparse.ArgumentParser(description='Convert .html file into .docx file with formatting') arg_parser.add_argument('filename_html', help='The .html file to be parsed') arg_parser.add_argument( @@ -675,7 +680,7 @@ def parse_html_string(self, html): default=None ) arg_parser.add_argument('--bs', action='store_true', - help='Attempt to fix html before parsing. Requires bs4. Default True') + help='Attempt to fix html before parsing. Requires bs4. Default True') args = vars(arg_parser.parse_args()) file_html = args.pop('filename_html') From 4a879571375756f389dc5add3b3c5e075a70d017 Mon Sep 17 00:00:00 2001 From: Maifee Ul Asad Date: Thu, 23 Dec 2021 21:44:36 +0600 Subject: [PATCH 3/5] added image dimension support --- htmldocx/h2d.py | 12 ++++++++---- tests/text1.html | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/htmldocx/h2d.py b/htmldocx/h2d.py index a9616ed..39dd7e0 100644 --- a/htmldocx/h2d.py +++ b/htmldocx/h2d.py @@ -25,6 +25,7 @@ from docx.enum.text import WD_COLOR, WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn +from docx.shared import Pt from bs4 import BeautifulSoup @@ -306,11 +307,11 @@ def handle_li(self): self.paragraph.paragraph_format.left_indent = Inches(min(list_depth * LIST_INDENT, MAX_INDENT)) self.paragraph.paragraph_format.line_spacing = 1 - def add_image_to_cell(self, cell, image): + def add_image_to_cell(self, cell, image,width,height): # python-docx doesn't have method yet for adding images to table cells. For now we use this paragraph = cell.add_paragraph() run = paragraph.add_run() - run.add_picture(image) + run.add_picture(image,width,height) def handle_img(self, current_attrs): if not self.include_images: @@ -318,6 +319,9 @@ def handle_img(self, current_attrs): self.skip_tag = 'img' return src = current_attrs['src'] + # added image dimension, interpreting values as pixel only + height = Pt(int(current_attrs['height'][:-2])) if 'height' in current_attrs else None + width = Pt(int(current_attrs['width'][:-2])) if 'width' in current_attrs else None # fetch image src_is_url = is_url(src) if src_is_url: @@ -331,9 +335,9 @@ def handle_img(self, current_attrs): if image: try: if isinstance(self.doc, docx.document.Document): - self.doc.add_picture(image) + self.doc.add_picture(image,width,height) else: - self.add_image_to_cell(self.doc, image) + self.add_image_to_cell(self.doc, image,width,height) except FileNotFoundError: image = None if not image: diff --git a/tests/text1.html b/tests/text1.html index 16e5a8f..6c79427 100644 --- a/tests/text1.html +++ b/tests/text1.html @@ -17,6 +17,8 @@

    A centered picture from url:

    A right aligned picture from url:

    A picture from url that's broken:

    +

    A picture with height and width:

    +

    A picture with height and width and centered:

    heading 1

    1. Ordered list first item
    2. From ee5c8be01ae4eb63e48b57c17a1cb853763a8fe1 Mon Sep 17 00:00:00 2001 From: Maifee Ul Asad <29339330+maifeeulasad@users.noreply.github.com> Date: Tue, 8 Mar 2022 16:07:45 +0600 Subject: [PATCH 4/5] [lint]: Add space between items. as per: https://github.com/pqzx/html2docx/pull/29#discussion_r821368306 --- htmldocx/h2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htmldocx/h2d.py b/htmldocx/h2d.py index 39dd7e0..fdb953d 100644 --- a/htmldocx/h2d.py +++ b/htmldocx/h2d.py @@ -307,7 +307,7 @@ def handle_li(self): self.paragraph.paragraph_format.left_indent = Inches(min(list_depth * LIST_INDENT, MAX_INDENT)) self.paragraph.paragraph_format.line_spacing = 1 - def add_image_to_cell(self, cell, image,width,height): + def add_image_to_cell(self, cell, image, width, height): # python-docx doesn't have method yet for adding images to table cells. For now we use this paragraph = cell.add_paragraph() run = paragraph.add_run() From bd6cc8f59f62c60f54f205c1c73055fbae476489 Mon Sep 17 00:00:00 2001 From: Maifee Ul Asad Date: Wed, 9 Mar 2022 19:33:19 +0600 Subject: [PATCH 5/5] lint: complete linting in h2d.py --- htmldocx/h2d.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/htmldocx/h2d.py b/htmldocx/h2d.py index fdb953d..3a6ff03 100644 --- a/htmldocx/h2d.py +++ b/htmldocx/h2d.py @@ -12,22 +12,24 @@ How to deal with block level style applied over table elements? e.g. text align """ +import argparse import enum -import re, argparse -import io, os +import io +import os +import re import urllib.request -from urllib.parse import urlparse from html.parser import HTMLParser +from urllib.parse import urlparse -import docx, docx.table +import docx +import docx.table +from bs4 import BeautifulSoup from docx import Document -from docx.shared import RGBColor, Pt, Inches from docx.enum.text import WD_COLOR, WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt - -from bs4 import BeautifulSoup +from docx.shared import RGBColor, Inches # values in inches INDENT = 0.25 @@ -311,7 +313,7 @@ def add_image_to_cell(self, cell, image, width, height): # python-docx doesn't have method yet for adding images to table cells. For now we use this paragraph = cell.add_paragraph() run = paragraph.add_run() - run.add_picture(image,width,height) + run.add_picture(image, width, height) def handle_img(self, current_attrs): if not self.include_images: @@ -335,9 +337,9 @@ def handle_img(self, current_attrs): if image: try: if isinstance(self.doc, docx.document.Document): - self.doc.add_picture(image,width,height) + self.doc.add_picture(image, width, height) else: - self.add_image_to_cell(self.doc, image,width,height) + self.add_image_to_cell(self.doc, image, width, height) except FileNotFoundError: image = None if not image: