mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
DOCX Output: Insert book cover as full page image at the start of the document
This commit is contained in:
parent
b1edf9c8b5
commit
3ce5236d60
@ -28,6 +28,10 @@ class DOCXOutput(OutputFormatPlugin):
|
|||||||
'EG. `123x321` to specify the width and height (in pts). '
|
'EG. `123x321` to specify the width and height (in pts). '
|
||||||
'This overrides any specified page-size.')),
|
'This overrides any specified page-size.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='docx_no_cover', recommended_value=False,
|
||||||
|
help=_('Do not insert the book cover as an image at the start of the document.'
|
||||||
|
' If you use this option, the book cover will be discarded.')),
|
||||||
|
|
||||||
OptionRecommendation(name='extract_to',
|
OptionRecommendation(name='extract_to',
|
||||||
help=_('Extract the contents of the generated %s file to the '
|
help=_('Extract the contents of the generated %s file to the '
|
||||||
'specified directory. The contents of the directory are first '
|
'specified directory. The contents of the directory are first '
|
||||||
@ -55,7 +59,7 @@ class DOCXOutput(OutputFormatPlugin):
|
|||||||
from calibre.ebooks.docx.writer.from_html import Convert
|
from calibre.ebooks.docx.writer.from_html import Convert
|
||||||
docx = DOCX(opts, log)
|
docx = DOCX(opts, log)
|
||||||
self.convert_metadata(oeb)
|
self.convert_metadata(oeb)
|
||||||
Convert(oeb, docx, self.mi)()
|
Convert(oeb, docx, self.mi, not opts.docx_no_cover)()
|
||||||
docx.write(output_path, self.mi)
|
docx.write(output_path, self.mi)
|
||||||
if opts.extract_to:
|
if opts.extract_to:
|
||||||
from calibre.ebooks.docx.dump import do_dump
|
from calibre.ebooks.docx.dump import do_dump
|
||||||
|
@ -27,6 +27,12 @@ def xml2str(root, pretty_print=False, with_tail=False):
|
|||||||
pretty_print=pretty_print, with_tail=with_tail)
|
pretty_print=pretty_print, with_tail=with_tail)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def page_size(opts):
|
||||||
|
width, height = PAPER_SIZES[opts.docx_page_size]
|
||||||
|
if opts.docx_custom_page_size is not None:
|
||||||
|
width, height = map(float, opts.docx_custom_page_size.partition('x')[0::2])
|
||||||
|
return width, height
|
||||||
|
|
||||||
def create_skeleton(opts, namespaces=None):
|
def create_skeleton(opts, namespaces=None):
|
||||||
namespaces = namespaces or DOCXNamespace().namespaces
|
namespaces = namespaces or DOCXNamespace().namespaces
|
||||||
def w(x):
|
def w(x):
|
||||||
@ -36,9 +42,7 @@ def create_skeleton(opts, namespaces=None):
|
|||||||
doc = E.document()
|
doc = E.document()
|
||||||
body = E.body()
|
body = E.body()
|
||||||
doc.append(body)
|
doc.append(body)
|
||||||
width, height = PAPER_SIZES[opts.docx_page_size]
|
width, height = page_size(opts)
|
||||||
if opts.docx_custom_page_size is not None:
|
|
||||||
width, height = map(float, opts.docx_custom_page_size.partition('x')[0::2])
|
|
||||||
width, height = int(20 * width), int(20 * height)
|
width, height = int(20 * width), int(20 * height)
|
||||||
def margin(which):
|
def margin(which):
|
||||||
return w(which), str(int(getattr(opts, 'margin_'+which) * 20))
|
return w(which), str(int(getattr(opts, 'margin_'+which) * 20))
|
||||||
|
@ -9,7 +9,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import re
|
import re
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from calibre.ebooks.docx.writer.container import create_skeleton
|
from calibre.ebooks.docx.writer.container import create_skeleton, page_size
|
||||||
from calibre.ebooks.docx.writer.styles import StylesManager, FloatSpec
|
from calibre.ebooks.docx.writer.styles import StylesManager, FloatSpec
|
||||||
from calibre.ebooks.docx.writer.links import LinksManager
|
from calibre.ebooks.docx.writer.links import LinksManager
|
||||||
from calibre.ebooks.docx.writer.images import ImagesManager
|
from calibre.ebooks.docx.writer.images import ImagesManager
|
||||||
@ -390,10 +390,11 @@ class Convert(object):
|
|||||||
a[href] { text-decoration: underline; color: blue }
|
a[href] { text-decoration: underline; color: blue }
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, oeb, docx, mi):
|
def __init__(self, oeb, docx, mi, add_cover):
|
||||||
self.oeb, self.docx = oeb, docx
|
self.oeb, self.docx, self.add_cover = oeb, docx, add_cover
|
||||||
self.log, self.opts = docx.log, docx.opts
|
self.log, self.opts = docx.log, docx.opts
|
||||||
self.mi = mi
|
self.mi = mi
|
||||||
|
self.cover_img = None
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer
|
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer
|
||||||
@ -411,6 +412,11 @@ class Convert(object):
|
|||||||
for item in self.oeb.spine:
|
for item in self.oeb.spine:
|
||||||
self.process_item(item)
|
self.process_item(item)
|
||||||
|
|
||||||
|
if self.add_cover and self.oeb.metadata.cover and unicode(self.oeb.metadata.cover[0]) in self.oeb.manifest.ids:
|
||||||
|
cover_id = unicode(self.oeb.metadata.cover[0])
|
||||||
|
item = self.oeb.manifest.ids[cover_id]
|
||||||
|
self.cover_img = self.images_manager.read_image(item.href)
|
||||||
|
|
||||||
all_blocks = self.blocks.all_blocks
|
all_blocks = self.blocks.all_blocks
|
||||||
remove_blocks = []
|
remove_blocks = []
|
||||||
for i, block in enumerate(all_blocks):
|
for i, block in enumerate(all_blocks):
|
||||||
@ -427,6 +433,8 @@ class Convert(object):
|
|||||||
self.blocks.apply_page_break_after()
|
self.blocks.apply_page_break_after()
|
||||||
self.blocks.resolve_language()
|
self.blocks.resolve_language()
|
||||||
|
|
||||||
|
if self.cover_img is not None:
|
||||||
|
self.cover_img = self.images_manager.create_cover_markup(self.cover_img, *page_size(self.opts))
|
||||||
self.lists_manager.finalize(all_blocks)
|
self.lists_manager.finalize(all_blocks)
|
||||||
self.styles_manager.finalize(all_blocks)
|
self.styles_manager.finalize(all_blocks)
|
||||||
self.write()
|
self.write()
|
||||||
@ -549,6 +557,8 @@ class Convert(object):
|
|||||||
self.docx.document, self.docx.styles, body = create_skeleton(self.opts)
|
self.docx.document, self.docx.styles, body = create_skeleton(self.opts)
|
||||||
self.blocks.serialize(body)
|
self.blocks.serialize(body)
|
||||||
body.append(body[0]) # Move <sectPr> to the end
|
body.append(body[0]) # Move <sectPr> to the end
|
||||||
|
if self.cover_img is not None:
|
||||||
|
self.images_manager.write_cover_block(body, self.cover_img)
|
||||||
self.styles_manager.serialize(self.docx.styles)
|
self.styles_manager.serialize(self.docx.styles)
|
||||||
self.images_manager.serialize(self.docx.images)
|
self.images_manager.serialize(self.docx.images)
|
||||||
self.fonts_manager.serialize(self.styles_manager.text_styles, self.docx.font_table, self.docx.embedded_fonts, self.docx.fonts)
|
self.fonts_manager.serialize(self.styles_manager.text_styles, self.docx.font_table, self.docx.embedded_fonts, self.docx.fonts)
|
||||||
|
@ -44,11 +44,7 @@ class ImagesManager(object):
|
|||||||
self.document_relationships = document_relationships
|
self.document_relationships = document_relationships
|
||||||
self.count = 0
|
self.count = 0
|
||||||
|
|
||||||
def add_image(self, img, block, stylizer, bookmark=None, as_block=False):
|
def read_image(self, href):
|
||||||
src = img.get('src')
|
|
||||||
if not src:
|
|
||||||
return
|
|
||||||
href = self.abshref(src)
|
|
||||||
if href not in self.images:
|
if href not in self.images:
|
||||||
item = self.oeb.manifest.hrefs.get(href)
|
item = self.oeb.manifest.hrefs.get(href)
|
||||||
if item is None or not isinstance(item.data, bytes):
|
if item is None or not isinstance(item.data, bytes):
|
||||||
@ -58,9 +54,17 @@ class ImagesManager(object):
|
|||||||
image_rid = self.document_relationships.add_image(image_fname)
|
image_rid = self.document_relationships.add_image(image_fname)
|
||||||
self.images[href] = Image(image_rid, image_fname, width, height, fmt, item)
|
self.images[href] = Image(image_rid, image_fname, width, height, fmt, item)
|
||||||
item.unload_data_from_memory()
|
item.unload_data_from_memory()
|
||||||
|
return self.images[href]
|
||||||
|
|
||||||
|
def add_image(self, img, block, stylizer, bookmark=None, as_block=False):
|
||||||
|
src = img.get('src')
|
||||||
|
if not src:
|
||||||
|
return
|
||||||
|
href = self.abshref(src)
|
||||||
|
rid = self.read_image(href).rid
|
||||||
drawing = self.create_image_markup(img, stylizer, href, as_block=as_block)
|
drawing = self.create_image_markup(img, stylizer, href, as_block=as_block)
|
||||||
block.add_image(drawing, bookmark=bookmark)
|
block.add_image(drawing, bookmark=bookmark)
|
||||||
return self.images[href].rid
|
return rid
|
||||||
|
|
||||||
def create_image_markup(self, html_img, stylizer, href, as_block=False):
|
def create_image_markup(self, html_img, stylizer, href, as_block=False):
|
||||||
# TODO: img inside a link (clickable image)
|
# TODO: img inside a link (clickable image)
|
||||||
@ -95,7 +99,7 @@ class ImagesManager(object):
|
|||||||
makeelement(parent, 'wp:simplePos', x='0', y='0')
|
makeelement(parent, 'wp:simplePos', x='0', y='0')
|
||||||
makeelement(makeelement(parent, 'wp:positionH', relativeFrom='margin'), 'wp:align').text = floating
|
makeelement(makeelement(parent, 'wp:positionH', relativeFrom='margin'), 'wp:align').text = floating
|
||||||
makeelement(makeelement(parent, 'wp:positionV', relativeFrom='line'), 'wp:align').text = 'top'
|
makeelement(makeelement(parent, 'wp:positionV', relativeFrom='line'), 'wp:align').text = 'top'
|
||||||
makeelement(parent, 'wp:extent', cx=str(width), cy=str(width))
|
makeelement(parent, 'wp:extent', cx=str(width), cy=str(height))
|
||||||
if fake_margins:
|
if fake_margins:
|
||||||
# DOCX does not support setting margins for inline images, so we
|
# DOCX does not support setting margins for inline images, so we
|
||||||
# fake it by using effect extents to simulate margins
|
# fake it by using effect extents to simulate margins
|
||||||
@ -108,22 +112,26 @@ class ImagesManager(object):
|
|||||||
makeelement(parent, 'wp:wrapTopAndBottom')
|
makeelement(parent, 'wp:wrapTopAndBottom')
|
||||||
else:
|
else:
|
||||||
makeelement(parent, 'wp:wrapSquare', wrapText='bothSides')
|
makeelement(parent, 'wp:wrapSquare', wrapText='bothSides')
|
||||||
makeelement(parent, 'wp:docPr', id=str(self.count), name=name, descr=html_img.get('alt') or name)
|
self.create_docx_image_markup(parent, name, html_img.get('alt') or name, img.rid, width, height)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def create_docx_image_markup(self, parent, name, alt, img_rid, width, height):
|
||||||
|
makeelement, namespaces = self.document_relationships.namespace.makeelement, self.document_relationships.namespace.namespaces
|
||||||
|
makeelement(parent, 'wp:docPr', id=str(self.count), name=name, descr=alt)
|
||||||
makeelement(makeelement(parent, 'wp:cNvGraphicFramePr'), 'a:graphicFrameLocks', noChangeAspect="1")
|
makeelement(makeelement(parent, 'wp:cNvGraphicFramePr'), 'a:graphicFrameLocks', noChangeAspect="1")
|
||||||
g = makeelement(parent, 'a:graphic')
|
g = makeelement(parent, 'a:graphic')
|
||||||
gd = makeelement(g, 'a:graphicData', uri=namespaces['pic'])
|
gd = makeelement(g, 'a:graphicData', uri=namespaces['pic'])
|
||||||
pic = makeelement(gd, 'pic:pic')
|
pic = makeelement(gd, 'pic:pic')
|
||||||
nvPicPr = makeelement(pic, 'pic:nvPicPr')
|
nvPicPr = makeelement(pic, 'pic:nvPicPr')
|
||||||
makeelement(nvPicPr, 'pic:cNvPr', id='0', name=name, descr=html_img.get('alt') or name)
|
makeelement(nvPicPr, 'pic:cNvPr', id='0', name=name, descr=alt)
|
||||||
makeelement(nvPicPr, 'pic:cNvPicPr')
|
makeelement(nvPicPr, 'pic:cNvPicPr')
|
||||||
bf = makeelement(pic, 'pic:blipFill')
|
bf = makeelement(pic, 'pic:blipFill')
|
||||||
makeelement(bf, 'a:blip', r_embed=img.rid)
|
makeelement(bf, 'a:blip', r_embed=img_rid)
|
||||||
makeelement(makeelement(bf, 'a:stretch'), 'a:fillRect')
|
makeelement(makeelement(bf, 'a:stretch'), 'a:fillRect')
|
||||||
spPr = makeelement(pic, 'pic:spPr')
|
spPr = makeelement(pic, 'pic:spPr')
|
||||||
xfrm = makeelement(spPr, 'a:xfrm')
|
xfrm = makeelement(spPr, 'a:xfrm')
|
||||||
makeelement(xfrm, 'a:off', x='0', y='0'), makeelement(xfrm, 'a:ext', cx=str(width), cy=str(height))
|
makeelement(xfrm, 'a:off', x='0', y='0'), makeelement(xfrm, 'a:ext', cx=str(width), cy=str(height))
|
||||||
makeelement(makeelement(spPr, 'a:prstGeom', prst='rect'), 'a:avLst')
|
makeelement(makeelement(spPr, 'a:prstGeom', prst='rect'), 'a:avLst')
|
||||||
return ans
|
|
||||||
|
|
||||||
def create_filename(self, href, fmt):
|
def create_filename(self, href, fmt):
|
||||||
fname = ascii_filename(urlunquote(posixpath.basename(href)))
|
fname = ascii_filename(urlunquote(posixpath.basename(href)))
|
||||||
@ -147,3 +155,31 @@ class ImagesManager(object):
|
|||||||
return item.data
|
return item.data
|
||||||
finally:
|
finally:
|
||||||
item.unload_data_from_memory(False)
|
item.unload_data_from_memory(False)
|
||||||
|
|
||||||
|
def create_cover_markup(self, img, width, height):
|
||||||
|
self.count += 1
|
||||||
|
makeelement, namespaces = self.document_relationships.namespace.makeelement, self.document_relationships.namespace.namespaces
|
||||||
|
|
||||||
|
root = etree.Element('root', nsmap=namespaces)
|
||||||
|
ans = makeelement(root, 'w:drawing', append=False)
|
||||||
|
parent = makeelement(ans, 'wp:anchor', **{'dist'+edge:'0' for edge in 'LRTB'})
|
||||||
|
parent.set('simplePos', '0'), parent.set('relativeHeight', '1'), parent.set('behindDoc',"0"), parent.set('locked', "0")
|
||||||
|
parent.set('layoutInCell', "1"), parent.set('allowOverlap', '1')
|
||||||
|
makeelement(parent, 'wp:simplePos', x='0', y='0')
|
||||||
|
makeelement(makeelement(parent, 'wp:positionH', relativeFrom='page'), 'wp:align').text = 'center'
|
||||||
|
makeelement(makeelement(parent, 'wp:positionV', relativeFrom='page'), 'wp:align').text = 'center'
|
||||||
|
width, height = map(pt_to_emu, (width, height))
|
||||||
|
makeelement(parent, 'wp:extent', cx=str(width), cy=str(height))
|
||||||
|
makeelement(parent, 'wp:effectExtent', l='0', r='0', t='0', b='0')
|
||||||
|
makeelement(parent, 'wp:wrapTopAndBottom')
|
||||||
|
self.create_docx_image_markup(parent, 'cover.jpg', _('Cover'), img.rid, width, height)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def write_cover_block(self, body, cover_image):
|
||||||
|
makeelement, namespaces = self.document_relationships.namespace.makeelement, self.document_relationships.namespace.namespaces
|
||||||
|
pbb = body[0].xpath('//*[local-name()="pageBreakBefore"]')[0]
|
||||||
|
pbb.set('{%s}val' % namespaces['w'], 'on')
|
||||||
|
p = makeelement(body, 'w:p', append=False)
|
||||||
|
body.insert(0, p)
|
||||||
|
r = makeelement(p, 'w:r')
|
||||||
|
r.append(cover_image)
|
||||||
|
@ -19,7 +19,7 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
|
|
||||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||||
Widget.__init__(self, parent, [
|
Widget.__init__(self, parent, [
|
||||||
'docx_page_size', 'docx_custom_page_size',
|
'docx_page_size', 'docx_custom_page_size', 'docx_no_cover',
|
||||||
])
|
])
|
||||||
for x in get_option('docx_page_size').option.choices:
|
for x in get_option('docx_page_size').option.choices:
|
||||||
self.opt_docx_page_size.addItem(x)
|
self.opt_docx_page_size.addItem(x)
|
||||||
|
@ -47,6 +47,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_docx_no_cover">
|
||||||
|
<property name="text">
|
||||||
|
<string>Do not insert &cover as image at start of document</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user