1# usage: python3 -I make-foreign.py <other-suite-soffice> <scratch-dir> [edges-only] 2# Writes foreign-v<N>.odt with the other suite saving at ODF version setting N 3# (/org.openoffice.Office.Common/Save/ODF DefaultVersion; 10/11 write 1.3, 12/3 write 1.4, 8 writes 1.2 extended), 4# using gen.bas as macro Standard.Module1.Gen in a throw-away profile, then derives the edge-*.odt variants 5# by editing the XML text of foreign-v10.odt and foreign-v11.odt (edges-only: only the variants). 6import os, re, subprocess, sys, time, zipfile 7from xml.sax.saxutils import escape 8 9soffice, scratch = sys.argv[1], sys.argv[2] 10here = os.path.dirname(os.path.abspath(__file__)) 11KINDS = "solid,solid_transp,gradient,gradient_transp,gradient_tgrad,hatch,hatch_nobg,bitmap_tile,bitmap_stretch,bitmap_once,none,shared" 12MODES = (10, 11, 12, 3, 8) 13XCU = """<?xml version="1.0" encoding="UTF-8"?> 14<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 15<item oor:path="/org.openoffice.Office.Common/Save/ODF"><prop oor:name="DefaultVersion" oor:op="fuse"><value>%d</value></prop></item> 16<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="MacroSecurityLevel" oor:op="fuse"><value>0</value></prop></item> 17</oor:items> 18""" 19MODULE = ('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">\n' 20 '<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">%s</script:module>\n') 21OPTS = ["--headless", "--nofirststartwizard", "--norestore", "--nologo"] 22 23def wait_gone(prof): 24 while subprocess.run(["pgrep", "-f", prof], capture_output=True).returncode == 0: 25 time.sleep(1) 26 27def generate(): 28 for mode in MODES: 29 prof = os.path.join(scratch, "profile_page_%d" % mode) 30 out = os.path.join(here, "foreign-v%d.odt" % mode) 31 env = dict(os.environ, PFF_KINDS=KINDS, PFF_OUT=out, PFF_PNG="file://" + os.path.join(here, "tile.png")) 32 ui = "-env:UserInstallation=file://" + prof 33 os.makedirs(os.path.join(prof, "user")) 34 open(os.path.join(prof, "user", "registrymodifications.xcu"), "w").write(XCU % mode) 35 subprocess.run([soffice, *OPTS, ui, "macro:///Standard.Module1.Main"], env=env, timeout=300) # creates the profile and its Standard library 36 wait_gone(prof) 37 open(os.path.join(prof, "user", "basic", "Standard", "Module1.xba"), "w").write(MODULE % escape(open(os.path.join(here, "gen.bas")).read())) 38 subprocess.run([soffice, *OPTS, ui, "macro:///Standard.Module1.Gen"], env=env, timeout=300) 39 wait_gone(prof) 40 assert os.path.getsize(out) > 0, "no output for mode %d" % mode 41 scrub(out) 42 43def scrub(path): 44 """replaces the generator string in meta.xml and the colour theme name in styles.xml by neutral ones""" 45 zi = zipfile.ZipFile(path); items = [(it, zi.read(it.filename)) for it in zi.infolist()]; zi.close() 46 zo = zipfile.ZipFile(path, "w") 47 for it, data in items: 48 if it.filename == "meta.xml": 49 data = re.sub(rb"<meta:generator>[^<]*</meta:generator>", b"<meta:generator>other suite</meta:generator>", data) 50 if it.filename == "styles.xml": 51 data = re.sub(rb'(<loext:theme-colors loext:name=")[^"]*"', rb'\1Other"', data) 52 zo.writestr(it, data, compress_type=zipfile.ZIP_STORED if it.filename == "mimetype" else zipfile.ZIP_DEFLATED) 53 zo.close() 54 55def rewrite(src, dst, fn): 56 zi = zipfile.ZipFile(os.path.join(here, src)); zo = zipfile.ZipFile(os.path.join(here, dst), "w") 57 for it in zi.infolist(): 58 data = zi.read(it.filename) 59 if it.filename == "styles.xml": 60 data = fn(data.decode("utf-8")).encode("utf-8") 61 zo.writestr(it, data, compress_type=zipfile.ZIP_STORED if it.filename == "mimetype" else zipfile.ZIP_DEFLATED) 62 zo.close() 63 64def sub1(t, old, new): 65 assert t.count(old) == 1, (old, t.count(old)) 66 return t.replace(old, new) 67 68def dp(t, name): 69 m = re.search(r'<style:style style:name="%s" style:family="drawing-page">.*?</style:style>' % name, t) 70 assert m, name 71 return m.group(0) 72 73def edges(): 74 # unresolved names: gradient (P_gradient, P_shared, P_shared2), hatch (P_hatch), transparency gradient only (P_gradient_tgrad) 75 def unknown(t): 76 t = sub1(t, dp(t, "Mdp3"), dp(t, "Mdp3").replace('"Gradient_20_1"', '"No_20_Such"')) 77 t = sub1(t, dp(t, "Mdp6"), dp(t, "Mdp6").replace('"Red_20_90_20_Degrees_20_Crossed_20_1"', '"No_20_Such"')) 78 return sub1(t, dp(t, "Mdp5"), dp(t, "Mdp5").replace('draw:opacity-name="TG1"', 'draw:opacity-name="No_20_Such"')) 79 rewrite("foreign-v10.odt", "edge-unknown-name.odt", unknown) 80 # empty properties element (P_gradient_transp), style without properties element (P_solid_transp) 81 def empty(t): 82 t = sub1(t, dp(t, "Mdp4"), '<style:style style:name="Mdp4" style:family="drawing-page"><style:drawing-page-properties/></style:style>') 83 return sub1(t, dp(t, "Mdp2"), '<style:style style:name="Mdp2" style:family="drawing-page"/>') 84 rewrite("foreign-v10.odt", "edge-empty.odt", empty) 85 # master page naming a drawing page style that is not defined (P_hatch) 86 rewrite("foreign-v10.odt", "edge-undefined.odt", lambda t: sub1(t, 'style:page-layout-name="Mpm7" draw:style-name="Mdp6"', 'style:page-layout-name="Mpm7" draw:style-name="No_20_Such"')) 87 # draw:fill="none" next to the page layout's fo:background-color (Standard, P_solid) 88 rewrite("foreign-v10.odt", "edge-none.odt", lambda t: sub1(t, dp(t, "Mdp1"), dp(t, "Mdp1").replace('draw:fill="solid"', 'draw:fill="none"'))) 89 # drawing page style among the common styles, before the hatch it names (P_hatch) 90 def common(t): 91 d = dp(t, "Mdp6") 92 return sub1(sub1(t, d, ""), "<office:styles>", "<office:styles>" + d) 93 rewrite("foreign-v10.odt", "edge-common.odt", common) 94 # both forms disagree: page layouts keep the gradient, the drawing page says solid red (P_gradient, P_shared, P_shared2); 95 # unusable drawing page next to a usable page layout fill (P_hatch_nobg, First Page) 96 def both(t): 97 t = sub1(t, dp(t, "Mdp3"), '<style:style style:name="Mdp3" style:family="drawing-page"><style:drawing-page-properties draw:fill="solid" draw:fill-color="#ff0000"/></style:style>') 98 return sub1(t, dp(t, "Mdp7"), dp(t, "Mdp7").replace('"Red_20_90_20_Degrees_20_Crossed_20_1"', '"No_20_Such"')) 99 rewrite("foreign-v11.odt", "edge-both.odt", both) 100 # page layout shared by a master page with a drawing page fill and one without (P_shared2; P_bitmap_stretch moved to the 101 # tile layout): the second keeps the page layout's own fallback 102 def shared(t): 103 t = sub1(t, 'style:display-name="P_shared2" style:page-layout-name="Mpm13" draw:style-name="Mdp3"', 'style:display-name="P_shared2" style:page-layout-name="Mpm13"') 104 return sub1(t, 'style:display-name="P_bitmap_stretch" style:page-layout-name="Mpm10" draw:style-name="Mdp9"', 'style:display-name="P_bitmap_stretch" style:page-layout-name="Mpm9"') 105 rewrite("foreign-v10.odt", "edge-shared-layout.odt", shared) 106 107if __name__ == "__main__": 108 if sys.argv[3:4] != ["edges-only"]: 109 generate() 110 edges() 111