Writer - How to merge multiples odt via python macro?

I have 3 .odt files with just one page and content filling this entire page. I need to merge these files into a single three-page file.

I haven’t found any native solution in python in libre office. If it did, it would be better.

The solution I’ve found so far was to create a blank main.odt file with the macro below. The bad thing about using ‘.uno:InsertDoc’ is that it inserts the text on the next line not the current line. So there is always a blank line before my text content.

This way the file has 6 pages 3 of content and 3 blank between the content pages.

Would it be possible to make ‘.uno:InsertDoc’ insert the text content without leaving that line blank?

Or is there a better solution?

my Macro:

# -*- coding: UTF-8 -*-
from __future__ import absolute_import, unicode_literals
import uno
from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS

from pathlib import Path

CTX = uno.getComponentContext()
SM = CTX.getServiceManager()
ODOC = XSCRIPTCONTEXT.getDocument()

def create_instance(name, with_context=False):
    if with_context:
        instance = SM.createInstanceWithContext(name, CTX)
    else:
        instance = SM.createInstance(name)
    return instance

def call_dispatch(doc, url, args=()):
    frame = doc.getCurrentController().getFrame()
    dispatch = create_instance('com.sun.star.frame.DispatchHelper')
    dispatch.executeDispatch(frame, url, '', 0, args)
    return


def set_page_style(lm=500, rm=500, tm=500, bm=500, landscape=False, pw=21000, ph=29700):
    oViewCursor = ODOC.CurrentController.getViewCursor()
    pageStyle = oViewCursor.PageStyleName
    oStyle = ODOC.StyleFamilies.getByName("PageStyles").getByName(pageStyle)

    oStyle.LeftMargin = lm
    oStyle.RightMargin = rm
    oStyle.TopMargin = tm
    oStyle.BottomMargin = bm

    oStyle.IsLandscape = landscape

    oStyle.Width = pw
    oStyle.Height = ph


def get_odt_files(myfilter='**/*'):
    url = ODOC.URL
    systempath = uno.fileUrlToSystemPath(url)
    absolute_path = Path(systempath).parent

    files_odt = list(filter(Path.is_file, absolute_path.glob(myfilter)))

    lst_file_str = []
    for file_path in files_odt:
        if file_path != Path(systempath):
            lst_file_str.append('file:///' + str(file_path))

    return lst_file_str


def merge_odt(*args):
    set_page_style(lm=500, rm=0, tm=1300, bm=0)
    lst_files = get_odt_files('*.odt')

    struct = uno.createUnoStruct('com.sun.star.beans.PropertyValue')

    total = len(lst_files)
    for i in range(total):
        struct.Name = 'Name'
        struct.Value = lst_files[i]

        call_dispatch(ODOC, '.uno:InsertDoc', tuple([struct]))
        
        if i != total-1:
            call_dispatch(ODOC, '.uno:InsertPagebreak')
    
    #call_dispatch(ODOC, '.uno:Print')

Complement

1 page file to be merged:
text1.odt

The solution is not the best, but it is resolved here.
I deleted the first line of each page after adding the files.

Thanks to everyone who helped me

# -*- coding: UTF-8 -*-
from __future__ import absolute_import, unicode_literals
import uno
from com.sun.star.beans import PropertyValue

from pathlib import Path

CTX = uno.getComponentContext()
SM = CTX.getServiceManager()
ODOC = XSCRIPTCONTEXT.getDocument()

def create_instance(name, with_context=False):
    if with_context:
        instance = SM.createInstanceWithContext(name, CTX)
    else:
        instance = SM.createInstance(name)
    return instance


def call_dispatch(doc, url, args=()):
    frame = doc.getCurrentController().getFrame()
    dispatch = create_instance('com.sun.star.frame.DispatchHelper')
    dispatch.executeDispatch(frame, url, '', 0, args)
    return


def set_page_style(lm=500, rm=500, tm=500, bm=500, bLandscape=False, pw=21000, ph=29700):
    oViewCursor = ODOC.CurrentController.getViewCursor()
    pageStyle = oViewCursor.PageStyleName
    oStyle = ODOC.StyleFamilies.getByName("PageStyles").getByName(pageStyle)

    oStyle.LeftMargin = lm #(lm = 0.5cm * 1000 --> 500)
    oStyle.RightMargin = rm
    oStyle.TopMargin = tm
    oStyle.BottomMargin = bm

    oStyle.IsLandscape = bLandscape

    #A4 pw=21000, ph=29700
    oStyle.Width = pw
    oStyle.Height = ph


def get_odt_files(myfilter='**/*'):
    thisDocUrl = ODOC.URL
    thisDocPath = uno.fileUrlToSystemPath(thisDocUrl)
    absolute_path = Path(thisDocPath).parent

    files_odt = list(filter(Path.is_file, absolute_path.glob(myfilter)))

    lst_file_str = []
    for file_path in files_odt:
        if file_path != Path(thisDocPath):
            fileUrl = uno.systemPathToFileUrl(str(file_path))
            lst_file_str.append(fileUrl)

    lst_file_str.sort()

    return lst_file_str


def _toProperties(**kwargs):
    props = []
    for key in kwargs:
        prop = PropertyValue()
        prop.Name = key
        prop.Value = kwargs[key]
        props.append(prop)
    return tuple(props)


def merge_odt(*args):
    lst_files = get_odt_files('*.odt')
    
    total = len(lst_files)
    for i in range(total):
        call_dispatch(ODOC, '.uno:InsertDoc', _toProperties(Name=lst_files[i], Filter='writer8'))

        if i != total-1:
            call_dispatch(ODOC, '.uno:InsertPagebreak')

    #remove the first line from each page
    for j in range(total):
        if j == 0:
            call_dispatch(ODOC, '.uno:GoToStartOfDoc')
        else:
            call_dispatch(ODOC, '.uno:GoToStartOfNextPage')
        call_dispatch(ODOC, '.uno:Delete')
        
    call_dispatch(ODOC, '.uno:GoToStartOfDoc')


def main(*args):
    set_page_style(lm=500, rm=0, tm=1300, bm=0)
    merge_odt()
    ODOC.store()
    call_dispatch(ODOC, '.uno:Print')

I tested .uno:InsertDoc with an input file that had no extra newlines, and the result was that a newline was not created in the result file. So on a line which contained text a, after the call, it contained ab followed by the rest of the inserted file contents.

In your case, the extra blank line may be coming from the source files, not magically created by the call.

To figure out what is going on, do everything manually first. The menu command is Insert → Text from File. Keep an eye on where you are putting the page breaks and what effect that has.

What I do when an UNO command results in unwanted empty space is to clean up afterwards with more dispatcher commands: .uno:Delete or .uno:SwBackspace depending on the direction. You may need to remember the location of the insertion and then return to that spot before issuing the dispatcher command.

The content of my odt is a table that occupies the entire page. In the original odt there is no blank line at the beginning, it only appears when I merge it.

Would you have sample code of how I position the cursor at the beginning of each of the pages and delete that line?

thanks

Tried a few files using the code below. Main.odt contains a few lines without a final return, file1.odt is likewise, file2.odt begins with a table occupying a few lines. Result is that file1 is appended to main, not on a new line, file2 begins on a new line (because of table?), the result fits on one page

def foo():

files = ["file1.odt", "file2.odt"]
oDoc = XSCRIPTCONTEXT.getDocument()
oCursor = oDoc.Text.createTextCursor()
#oCursor.goRight(1,False)  

for sFileName in files:
    
    sFileName = "file://"+"/home/user/" + sFileName
    oCursor.gotoEnd(False)
    oCursor.insertDocumentFromURL(sFileName, ())

Another option for future consultations:

def foo2(*args):
    ODOC = XSCRIPTCONTEXT.getDocument()
    sFileName = 'file:///home/MY_USER/myfile.odt'

    struct = uno.createUnoStruct('com.sun.star.beans.PropertyValue')
    struct.Name = 'Filter'
    struct.Value = "writer8"

    oText = ODOC.Text
    oViewCursor = ODOC.CurrentController.getViewCursor()
    oTextCursor = oText.createTextCursorByRange(oViewCursor.getStart())
    oTextCursor.insertDocumentFromURL(sFileName, tuple([struct]))

Looking at your code…it may or may not work. It depends on “time” between update of libre office. One thing Office does not have is they removed the record marco found from in the original open source code (Microsoft Office or WordPerfect). Therefore you can’t compare nomenclature of similar function calls.

This answer makes no sense at all.