1# Known limitations of the import these documents exercise: 2# - a fill is ignored when its gradient, hatch or bitmap definition comes after the style that uses it; 3# - multi-stop gradients, unnamed direct gradients and hatches, and the opacity of a direct bitmap are not imported; 4# - draw:angle with a unit suffix such as "30deg" is read as 0 (existing behaviour of the gradient import, unrelated to the fill element). 5# usage: python3 -I make.py <other-suite-soffice> <scratch-dir> 6# Writes foreign-v8.odt, foreign-v3.odt, foreign-v11.odt with the other suite saving at ODF version setting 8, 3, 11 7# (/org.openoffice.Office.Common/Save/ODF DefaultVersion), using gen.bas as macro Standard.Module1.Gen in a throw-away profile, 8# then derives the edge-*.odt variants by editing the XML text of foreign-v8.odt. 9import os, re, subprocess, sys, time, zipfile 10from xml.sax.saxutils import escape 11 12soffice, scratch = sys.argv[1], sys.argv[2] 13here = os.path.dirname(os.path.abspath(__file__)) 14KINDS = "solid,solid_transp,gradient,gradient_transp,hatch,hatch_nobg,bitmap_tile,bitmap_stretch,bitmap_once,bitmap_transp" 15XCU = """<?xml version="1.0" encoding="UTF-8"?> 16<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"> 17<item oor:path="/org.openoffice.Office.Common/Save/ODF"><prop oor:name="DefaultVersion" oor:op="fuse"><value>%d</value></prop></item> 18<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="MacroSecurityLevel" oor:op="fuse"><value>0</value></prop></item> 19</oor:items> 20""" 21MODULE = ('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">\n' 22 '<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">%s</script:module>\n') 23OPTS = ["--headless", "--nofirststartwizard", "--norestore", "--nologo"] 24 25def wait_gone(prof): 26 while subprocess.run(["pgrep", "-f", prof], capture_output=True).returncode == 0: 27 time.sleep(1) 28 29for mode in (8, 3, 11): 30 prof = os.path.join(scratch, "profile_%d" % mode) 31 env = dict(os.environ, PFF_KINDS=KINDS, PFF_OUT=os.path.join(here, "foreign-v%d.odt" % mode), 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(env["PFF_OUT"]) > 0, "no output for mode %d" % mode 41 42def scrub(name): 43 """replaces the generator string in meta.xml and the colour theme name in styles.xml by neutral ones""" 44 path = os.path.join(here, name) 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 55for mode in (8, 3, 11): 56 scrub("foreign-v%d.odt" % mode) 57 58def rewrite(src, dst, fn): 59 zi = zipfile.ZipFile(os.path.join(here, src)); zo = zipfile.ZipFile(os.path.join(here, dst), "w") 60 for it in zi.infolist(): 61 data = zi.read(it.filename) 62 if it.filename in ("content.xml", "styles.xml"): 63 data = fn(it.filename, data.decode("utf-8")).encode("utf-8") 64 zo.writestr(it, data, compress_type=zipfile.ZIP_STORED if it.filename == "mimetype" else zipfile.ZIP_DEFLATED) 65 zo.close() 66 67def sub1(t, old, new): 68 assert t.count(old) == 1, (old, t.count(old)) 69 return t.replace(old, new) 70 71def style_xml(t, name): 72 m = re.search(r'<style:style style:name="%s".*?</style:style>' % re.escape(name), t, re.S) 73 assert m, name 74 return m 75 76# named gradient reference that does not exist 77rewrite("foreign-v8.odt", "edge-unknown-name.odt", lambda n, t: t.replace('draw:fill-gradient-name="Gradient_20_1"', 'draw:fill-gradient-name="No_20_Such"') if n == "styles.xml" else t) 78# empty element in a paragraph style that has fo:background-color 79def empty(n, t): 80 if n != "styles.xml": return t 81 m = style_xml(t, "S_5f_gradient") 82 return t.replace(m.group(0), re.sub(r'<loext:graphic-properties[^>]*/>', '<loext:graphic-properties/>', m.group(0))) 83rewrite("foreign-v8.odt", "edge-empty.odt", empty) 84# same element in non-paragraph styles (text style, table cell style) plus a paragraph that uses them 85EXTRA_TEXT = '<style:style style:name="T9" style:family="text"><loext:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/></style:style>' 86EXTRA_CELL = '<style:style style:name="C9" style:family="table-cell"><loext:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/></style:style>' 87EXTRA_FRAME = '<style:style style:name="G9" style:family="graphic"><loext:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/></style:style>' 88def nonpara(n, t): 89 if n == "styles.xml": 90 return t.replace("</office:styles>", EXTRA_FRAME + "</office:styles>", 1) 91 return t.replace("</office:automatic-styles>", EXTRA_TEXT + EXTRA_CELL + "</office:automatic-styles>", 1) 92rewrite("foreign-v8.odt", "edge-nonparagraph.odt", nonpara) 93# duplicate element (second one must lose) and nested element 94def dup(n, t): 95 if n != "styles.xml": return t 96 m = style_xml(t, "S_5f_solid") 97 s = m.group(0).replace('<style:paragraph-properties', '<loext:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/><style:paragraph-properties', 1) 98 m2 = style_xml(t, "S_5f_hatch") 99 h = m2.group(0).replace('draw:fill-hatch-solid="true"/>', 'draw:fill-hatch-solid="true"><loext:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/></loext:graphic-properties>', 1) 100 return t.replace(m.group(0), s).replace(m2.group(0), h) 101rewrite("foreign-v8.odt", "edge-duplicate-nested.odt", dup) 102# the native form and the extension element in one style, both orders 103def both(n, t): 104 if n != "styles.xml": return t 105 m = style_xml(t, "S_5f_solid") 106 native = '<style:graphic-properties draw:fill="solid" draw:fill-color="#ff0000"/>' 107 s1 = m.group(0).replace('<loext:graphic-properties', native + '<loext:graphic-properties', 1) 108 m2 = style_xml(t, "S_5f_solid_5f_fill") if 'S_5f_solid_5f_fill' in t else style_xml(t, "S_5f_solid_5f_transp") 109 s2 = m2.group(0).replace('<style:paragraph-properties', native + '<style:paragraph-properties', 1) 110 return t.replace(m.group(0), s1).replace(m2.group(0), s2) 111rewrite("foreign-v8.odt", "edge-both-forms.odt", both) 112# an attribute of the extension element that no fill property maps to 113def unknown_attr(n, t): 114 if n != "styles.xml": return t 115 m = style_xml(t, "S_5f_solid") 116 return t.replace(m.group(0), m.group(0).replace('<loext:graphic-properties ', '<loext:graphic-properties loext:future-attribute="x" ', 1)) 117rewrite("foreign-v8.odt", "edge-unknown-attr.odt", unknown_attr) 118print("done") 119