Change 2 or more LibreOffice documents to have the exact same styling/formatting

Say I have a directory with about 100 .rtf files I edit with LibreOffice Writer.

I desire that all files in that directory will have the exact same basic rich text styling-directives, for example:

* font-family: Ubuntu             # All text in all files is now Ubuntu;
* font-size: 12px                 # All text in all files is now 12px big;
h1: 28px                          # All h1's are now 28px big;
if font-size: 18px {make it 22px} # All text with font-size 18px is now 22px;

And so forth... So, basically I want to change all files at once. Is such "bulk-styling" possible?

Maybe it's possible with the CLI somehow?


Use LibreOffice Tools Instead of Command Line

When all you have are command line tools everything looks like a command line problem. I've decided to write this answer using LibreOffice macros:

  1. Use a command line loop to process every Writer document in a "headless" environment.
  2. Run macro to change .rtf (Rich Text Format) Writer document file.
  3. Macro saves file and exit
  4. Loop back to 1.

Create Test Data

Create two or more files containing:

richtext2.png

Create script ~/Downloads/copy-rtf.sh containing:

cp ~/Documents/*.rtf ~/Downloads

Mark as executable using

chmod a+x ~/Downloads/copy-rtf.sh
  • During development and testing, the macros modifying *.rtf files will run against ~/Downloads directory.
  • Before each test type cd ~/Downloads and run ./copy-rtf.sh
  • After output is perfect, they are copied back into live directory.

The Downloads directory is used because:

  • everyone has a ~/Downloads
  • it gets added to regularly and manually emptied periodically
  • it is more permanent than /tmp/ directory which may not persist across reboots.

Run Macro in Headless Environment

Using this Stack Exchange answer, run Libreoffice Writer from the command line and pass it a global macro name to execute:

soffice -headless -invisible "vnd.sun.star.script:Standard.Module1.MySubroutine? language=Basic&location=application"

If the above method doesn't work, another method can be tried:

soffice "macro:///Standard.SaveCSV.Main" $1

Install Java Runtime Environment

To run macros you need Java Runtime Environment (JRE) installed. The developer's web page has instructions for downloading and installing manually.

However this AU Q&A: https://askubuntu.com/a/728153/307523 suggests it is as simple as:

sudo apt-add-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer oracle-java8-set-default

I tried the AU Q&A method and after the first step of adding the PPA a splash screen appears with additional information. The most helpful is a link to setting up JRE 8 on Debian systems.

The third step of installing JRE 8 requires you to use Tab and Enter to accept the License Agreement. Your machine will pause for a few minutes during the heaviest part of the installation routine.

Now open LibreOffice and select Tools -> Options -> LibreOffice -> Advanced and setup this screen:

LO JRE8 Advanced Setup.png

Click the options for:

  • Use a Java runtime environment
  • Oracle Corporation 1.8.0_161
  • Enable macro recording (experimental)
  • Click OK
  • You will be asked to restart, click "Restart Now".

LibreOffice Writer Macro

The macro will read through the entire document and:

  • change font name to Ubuntu.
  • If heading 1 set font size to 28
  • else if font size is 18 set to 22
  • else set font size to 12

The macro will save document and exit LibreOffice Writer.

Turn Off Dialog

Do a file save and this dialog comes up:

LO Writer turn off RTF dialog.png

Turn this message off as show in the screen. The macro may not run properly if this option is on.

Macro Contents

I spent a few days attempting to record a macro using "Tools" -> "Macros" -> "Record Macro" -> "Basic". At first it seemed promising but the recorded macro had inconsistent behavior and had to be abandoned for a hand written basic macro. A found help in Stack Overflow for an expert there to help me with the basic basic coding. Here is the result:

Sub ChangeAllFonts
    rem - Change all font names to Ubuntu.
    rem - If heading 1 set font size to 28
    rem - else if font size is 18 set to 22
    rem - else set font size to 12
    rem - The macro will save document and exit LibreOffice Writer.
    Dim oDoc As Object
    Dim oParEnum As Object, oPar As Object, oSecEnum As Object, oSec As Object
    Dim oFamilies As Object, oParaStyles As Object, oStyle As Object
    oDoc = ThisComponent
    oParEnum = oDoc.Text.createEnumeration()
    Do While oParEnum.hasMoreElements()
      oPar = oParEnum.nextElement()
      If oPar.supportsService("com.sun.star.text.Paragraph") Then
        oSecEnum = oPar.createEnumeration()
        Do While oSecEnum.hasMoreElements()
          oSec = oSecEnum.nextElement()
          If oSec.TextPortionType = "Text" Then
            If oSec.ParaStyleName = "Heading 1" Then
                rem ignore for now
            ElseIf oSec.CharHeight = 18 Then
                oSec.CharHeight = 22.0
            Else
                oSec.CharHeight = 12.0
            End If
          End If
        Loop
      End If
    Loop
    oFamilies = oDoc.getStyleFamilies()
    oParaStyles = oFamilies.getByName("ParagraphStyles")
    oStyle = oParaStyles.getByName("Heading 1")
    oStyle.setPropertyValue("CharHeight", 28.0)
    FileSave
    StarDesktop.terminate()
End Sub

rem Above subroutine is missing call to UbuntuFontName ()
rem also it is calling oStyle.setPropertyValue("CharHeight", 28.0)
rem which may cause problems. Will test. Also StarDesktop.terminate ()
rem is known to cause problems and will likely be reworked with a
rem a dialog box telling operator the program is finished and maybe
rem to press <Alt>+<F4>.

rem ========= Original code below for possible recycling ===========

Sub AllFonts
rem - change all font names to Ubuntu.
rem - If heading 1 set font size to 28
rem - else if font size is 18 set to 22
rem - else set font size to 12

rem The macro will save document and exit LibreOffice Writer.

Dim CharHeight As Long, oSel as Object, oTC as Object
Dim CharStyleName As String
Dim oParEnum as Object, oPar as Object, oSecEnum as Object, oSec as Object
Dim oVC as Object, oText As Object
Dim oParSection        'Current Section
      
oText = ThisComponent.Text
oSel = ThisComponent.CurrentSelection.getByIndex(0) 'get the current selection
oTC = oText.createTextCursorByRange(oSel)           ' and span it with a cursor

rem Scan the cursor range for chunks of given text size.
rem (Doesn't work - affects the whole document)

oParEnum = oTC.Text.createEnumeration()
Do While oParEnum.hasMoreElements()
  oPar = oParEnum.nextElement()
  If oPar.supportsService("com.sun.star.text.Paragraph") Then
    oSecEnum = oPar.createEnumeration()
    oParSection = oSecEnum.nextElement()
    Do While oSecEnum.hasMoreElements()
      oSec = oSecEnum.nextElement()
      If oSec.TextPortionType = "Text" Then
        CharStyleName = oParSection.CharStyleName
        CharHeight = oSec.CharHeight
        if CharStyleName = "Heading 1" Then
            oSec.CharHeight = 28
        elseif CharHeight = 18 Then
            oSec.CharHeight = 22
        else
            oSec.CharHeight = 12
        End If
      End If
    Loop
  End If

Loop

FileSave
stardesktop.terminate()

End Sub


Sub UbuntuFontName
rem ----------------------------------------------------------------------
rem define variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem get access to the document
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

rem ----------- Select all text ------------------------------------------
dispatcher.executeDispatch(document, ".uno:SelectAll", "", 0, Array())

rem ----------- Change all fonts to Ubuntu -------------------------------
dim args5(4) as new com.sun.star.beans.PropertyValue
args5(0).Name = "CharFontName.StyleName"
args5(0).Value = ""
args5(1).Name = "CharFontName.Pitch"
args5(1).Value = 2
args5(2).Name = "CharFontName.CharSet"
args5(2).Value = -1
args5(3).Name = "CharFontName.Family"
args5(3).Value = 0
args5(4).Name = "CharFontName.FamilyName"
args5(4).Value = "Ubuntu"

dispatcher.executeDispatch(document, ".uno:CharFontName", "", 0, args5())

end sub


sub FileSave
rem ----------------------------------------------------------------------
rem define variables
dim document   as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem get access to the document
document   = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

rem ----------------------------------------------------------------------
dispatcher.executeDispatch(document, ".uno:Save", "", 0, Array())

end sub

Here's a one off approach using Libre Office. It's not batch, but it may help inspire other answers.

Open an rtf file that has the Ubuntu font, H1 at 28 pt, some text at 12pt and some at 18 pt.

Here's an example:

test rtf

The following steps will apply the change requested in your question "if font-size: 18px {make it 22px} # All text with font-size 18px is now 22px;"

Click Edit→ Find & Replace on the menu or hit CtrlH Other options Click Search For box then attributes button Font size check box then Format button select 18 pt from the scroll box on far right Click Replace with box then Format button and select 22 pt from the scroll box on far right

FindNreplace

Click Replace All

the applicable line that changed was:

\par \pard\plain \s0\ql\widctlpar\hyphpar0\ltrpar\cf1\kerning1\dbch\af7\langfe1081\dbch\af7\afs24\alang1081\loch\f3\fs24\lang1033\ql\widctlpar\hyphpar0\ltrpar{\rtlch \ltrch\loch\fs36\loch\f6

the fs36 changed to fs44

the only other field that changed was the revtime field which you may or may not want to update:

{\revtim\yr2018\mo3\dy31\hr22\min19}

Knowing what changed provides us with a model for developing a batch approach. It's likely possible to Record a Macro that does this upon opening a document or to develop a script that makes the changes as desired.


There are some great clues on how one might go about this in the RTF specification.

Here's my analysis of the issue.

Accomplishing this using the CLI as you surmise would seem to be the simplest approach as I've not seen any GUI based applications that can handle this sort of batch conversion. It appears that you can simply modify the header:

The header has the following syntax:

<header>
    \rtf <charset> \deff? <fonttbl> <filetbl>? <colortbl>? <stylesheet>? <listtables>? <revtbl>?

Each of the various header tables should appear, if they exist, in the above order. Document properties can occur before and between the header tables. A property must be defined before being referenced. Specifically:

* The style sheet must occur before any style usage.

* The font table must precede any reference to a font.

* The \deff keyword must precede any text without an explicit reference to a font, because it specifies the font to use in such cases.

Personally, upon review of this information it occurs to me that everything you are attempting to do looks like it's supported in the header, from font selection to style.

There are tools available to help you with this process which I will outline below as I don't have an example of the document style you have nor the document style you want and a more generic answer will likely be of more use to the community than one targeted to your exact situation.

grep will be useful to parse the existing files to be converted and a sample of the target style for existing <fonttbl> and
<stylesheet> selections. Having determined what you actually have, you should be able to write a simple script utilizing sed to replace the existing header content with the desired header content. There are numerous examples of how to iterate through files in a bash script (example) and how to utilize sed (example) freely available if you are unfamiliar with these concepts.

There are also one line options to replace a string in a file. Some may work better than others depending on your use case. Depending on the content of your files it may or may not make sense to simple replace every instance of fs36 with fs44 Which shell you are using may also have bearing on how best to write your expressions. Depending upon the complexity and content of your documents you may be better off using sed, perl or grep or perhaps even a combination of them. As this has become a programming question it's best to refer you to https://stackoverflow.com/questions/15402770/how-to-grep-and-replace where you'll find easily a 1/2 dozen different approaches, one of which is likely to suit your needs perfectly.

For example, if you wish to apply these changes system wide,

find /path/to/files -type f -exec sed -i 's/oldstring/newstring/g' {} \; as provided byrezizter Is likely the best.

If you wish to contain your changes to a single directory,

grep -rl matchstring somedir/ | xargs sed -i 's/fs36/fs44/g'as provided by billtian is an excellent choice.

To be on the safe side you should preprocess the files to insure that any changes that you may make won't have unintended consequences. For instance:

<!-- language: lang-bash -->

    #!/bin/bash
    for f in *.rtf 
        do
        echo $f
        grep fs36
        done

The above will display the lines containing the search string fs36 for each .rtf file in the directory.

Edit:

The most recent specification can be obtained here. I don't see any changes that should impact this approach.