1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22# usage: make-consumer.py -- (re)generate the ODF 1.4 consumer test packages in fixtures/t14/consumer/ 23# make-consumer.py --report <val.json> -- print the table of validate.py results for them 24# Each CASES entry is one construct from the ODF 1.4 additions inside an otherwise plain document, written with 25# office:version and manifest:version 1.4, plus (unless base is None) a "<name>-base" control without the construct. 26# "fields" fills the ${...} placeholders of the case; "base" overrides them for the control (all others become empty). 27import json, os, struct, sys, zipfile, zlib 28from string import Template 29 30OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "consumer") 31NS = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", 32 "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0", "table": "urn:oasis:names:tc:opendocument:xmlns:table:1.0", 33 "draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", 34 "svg": "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0", "xlink": "http://www.w3.org/1999/xlink", 35 "chart": "urn:oasis:names:tc:opendocument:xmlns:chart:1.0", "number": "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0", 36 "dc": "http://purl.org/dc/elements/1.1/", "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0"} 37NSDECL = " ".join('xmlns:%s="%s"' % kv for kv in NS.items()) 38MT = "application/vnd.oasis.opendocument." 39# ext: (media type, body element, wrapper around the case's "content", has page layout and master page) 40KINDS = {"odt": (MT + "text", "office:text", "${content}", True), 41 "ods": (MT + "spreadsheet", "office:spreadsheet", "${content}", True), 42 "odg": (MT + "graphics", "office:drawing", '<draw:page draw:name="page1" draw:master-page-name="Default">${content}</draw:page>', True), 43 "odc": (MT + "chart", "office:chart", "${content}", False)} 44TXT = "<text:p>Plain text.</text:p>" 45CELLS = '<table:table table:name="Sheet1"><table:table-column/><table:table-row>%s</table:table-row></table:table>' 46RECT = 'svg:x="1cm" svg:y="1cm" svg:width="6cm" svg:height="4cm"' 47GEOM = '<draw:custom-shape %s><draw:enhanced-geometry svg:viewBox="0 0 21600 21600" draw:type="rectangle" ${eg}>${handles}</draw:enhanced-geometry></draw:custom-shape>' % RECT 48FRAME = '<draw:frame draw:name="F1" text:anchor-type="paragraph" svg:width="4cm"%s><draw:text-box fo:min-height="2cm"%s><text:p>Box</text:p></draw:text-box></draw:frame>' 49PSTYLE = '<style:style style:name="%s" style:family="paragraph">%s</style:style>' 50GSTYLE = '<style:style style:name="%s" style:family="graphic">%s</style:style>' 51 52 53def png(): 54 """A 1x1 RGB PNG.""" 55 def chunk(t, d): 56 c = t + d 57 return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff) 58 return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) 59 + chunk(b"IDAT", zlib.compress(b"\x00\xff\x00\x00")) + chunk(b"IEND", b"")) 60 61 62def pstyle(n, tag): 63 """Paragraph style n with the given properties element.""" 64 return PSTYLE % (n, tag) 65 66 67# name, ext, construct, content, auto (content.xml automatic styles), styles (styles.xml office:styles), fields, base 68CASES = [ 69 dict(name="value-type-error", ext="ods", construct='office:value-type="error" with office:string-value, text:p child', 70 content=CELLS % '<table:table-cell ${c}><text:p>#DIV/0!</text:p></table:table-cell>', 71 fields={"c": 'office:value-type="error" office:string-value="#DIV/0!"'}, base={"c": 'office:value-type="string"'}), 72 dict(name="value-type-error-nostring", ext="ods", construct='office:value-type="error" without office:string-value, text:p child', 73 content=CELLS % '<table:table-cell ${c}><text:p>#N/A</text:p></table:table-cell>', 74 fields={"c": 'office:value-type="error"'}, base=None), 75 dict(name="extrusion-metal-type", ext="odg", construct='draw:extrusion-metal-type="draw:default" on draw:enhanced-geometry (value arbitrary: the RNG type is namespacedToken with a declared prefix)', 76 content=GEOM, fields={"eg": 'draw:extrusion="true" ${m}', "m": 'draw:extrusion-metal="true" draw:extrusion-metal-type="draw:default"', "handles": ""}, 77 base={"eg": 'draw:extrusion="true" draw:extrusion-metal="true"', "handles": ""}), 78 dict(name="extrusion-specularity", ext="odg", construct='draw:extrusion-specularity="150%" on draw:enhanced-geometry', 79 content=GEOM, fields={"eg": 'draw:extrusion="true" draw:extrusion-specularity="${v}"', "v": "150%", "handles": ""}, 80 base={"eg": 'draw:extrusion="true" draw:extrusion-specularity="80%"', "handles": ""}), 81 dict(name="writing-mode-sideways", ext="odt", construct='style:writing-mode sideways-rl / sideways-lr on paragraph styles, sideways-rl on graphic-properties', 82 auto=pstyle("P1", '<style:paragraph-properties style:writing-mode="${w1}"/>') + pstyle("P2", '<style:paragraph-properties style:writing-mode="${w2}"/>') 83 + GSTYLE % ("G1", '<style:graphic-properties style:writing-mode="${w3}"/>'), 84 content='<text:p text:style-name="P1">One</text:p><text:p text:style-name="P2">Two</text:p><text:p>' + FRAME % (' draw:style-name="G1"', "") + '</text:p>', 85 fields={"w1": "sideways-rl", "w2": "sideways-lr", "w3": "sideways-rl"}, base={"w1": "lr-tb", "w2": "rl-tb", "w3": "lr-tb"}), 86 dict(name="break-even-odd-page", ext="odt", construct='fo:break-before="even-page" / "odd-page" on paragraph styles', 87 auto=pstyle("P1", '<style:paragraph-properties fo:break-before="${b1}"/>') + pstyle("P2", '<style:paragraph-properties fo:break-before="${b2}"/>'), 88 content='<text:p text:style-name="P1">Even</text:p><text:p text:style-name="P2">Odd</text:p>', 89 fields={"b1": "even-page", "b2": "odd-page"}, base={"b1": "page", "b2": "auto"}), 90 dict(name="vertical-rel-page-content", ext="odt", construct='style:vertical-rel="page-content-bottom" / "page-content-top" on graphic-properties of anchored text-box frames', 91 auto=GSTYLE % ("G1", '<style:graphic-properties style:vertical-pos="bottom" style:vertical-rel="${r1}"/>') 92 + GSTYLE % ("G2", '<style:graphic-properties style:vertical-pos="top" style:vertical-rel="${r2}"/>'), 93 content='<text:p>' + FRAME % (' draw:style-name="G1"', "") + FRAME.replace("F1", "F2") % (' draw:style-name="G2"', "") + '</text:p>', 94 fields={"r1": "page-content-bottom", "r2": "page-content-top"}, base={"r1": "page-content", "r2": "page-content"}), 95 dict(name="repeat-scale", ext="odg", construct='style:repeat="scale" on a graphic style with a bitmap fill', 96 auto=GSTYLE % ("gr1", '<style:graphic-properties draw:fill="bitmap" draw:fill-image-name="Img" style:repeat="${r}"/>'), 97 styles='<draw:fill-image draw:name="Img" xlink:href="Pictures/p.png" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>', 98 content='<draw:rect draw:style-name="gr1" %s/>' % RECT, fields={"r": "scale"}, base={"r": "stretch"}, extra={"Pictures/p.png": png()}), 99 dict(name="svg-rx-ry", ext="odt", construct='svg:rx / svg:ry on a draw:text-box in a draw:frame and on an office:annotation', 100 content='<text:p>' + FRAME % ("", ' ${r}') + 'x<office:annotation ${r}><dc:creator>A</dc:creator><text:p>Note</text:p></office:annotation></text:p>', 101 fields={"r": 'svg:rx="0.3cm" svg:ry="0.2cm"'}, base={}), 102 dict(name="table-in-shape", ext="odg", construct='table:table as a child of draw:rect', 103 content='<draw:rect %s>${t}</draw:rect>' % RECT, 104 fields={"t": '<table:table table:name="T1"><table:table-column/><table:table-row><table:table-cell office:value-type="string"><text:p>cell</text:p></table:table-cell></table:table-row></table:table>'}, 105 base={"t": "<text:p>text</text:p>"}), 106 dict(name="annotation-in-page", ext="odg", construct='office:annotation as a child of draw:page', 107 content='<draw:rect %s><text:p>shape</text:p></draw:rect>${a}' % RECT, 108 fields={"a": '<office:annotation svg:x="8cm" svg:y="1cm" svg:width="4cm" svg:height="2cm"><dc:creator>A</dc:creator><dc:date>2026-11-03T10:00:00</dc:date><text:p>Note</text:p></office:annotation>'}, base={}), 109 dict(name="background-fill-paragraph", ext="odt", construct='style:background-fill (solid, #ff0000) in style:paragraph-properties', 110 auto=pstyle("P1", '<style:paragraph-properties>${bf}</style:paragraph-properties>'), 111 content='<text:p text:style-name="P1">Red</text:p>', fields={"bf": '<style:background-fill draw:fill="solid" draw:fill-color="#ff0000"/>'}, base={}), 112 dict(name="background-fill-cell", ext="ods", construct='style:background-fill (solid, #ff0000) in style:table-cell-properties', 113 auto='<style:style style:name="ce1" style:family="table-cell"><style:table-cell-properties>${bf}</style:table-cell-properties></style:style>', 114 content=CELLS % '<table:table-cell table:style-name="ce1" office:value-type="string"><text:p>Red</text:p></table:table-cell>', 115 fields={"bf": '<style:background-fill draw:fill="solid" draw:fill-color="#ff0000"/>'}, base={}), 116 dict(name="num-list-format", ext="odt", construct='number:num-list-format / number:num-list-label in office:styles, style:num-list-format-name on text:list-level-style-number', 117 styles='${nlf}<text:list-style style:name="L1"><text:list-level-style-number text:level="1" style:num-format="1" ${nm}/></text:list-style>', 118 content='<text:list text:style-name="L1"><text:list-item><text:p>One</text:p></text:list-item></text:list>', 119 fields={"nlf": '<number:num-list-format style:name="NLF1"><number:text>(</number:text><number:num-list-label text:level="1"/><number:text>)</number:text></number:num-list-format>', 120 "nm": 'style:num-list-format-name="NLF1"'}, base={}), 121 dict(name="filter-color-condition", ext="ods", construct='table:filter-condition with table:data-type="background-color" and "text-color", table:value a colour', 122 content=CELLS % '<table:table-cell office:value-type="string"><text:p>a</text:p></table:table-cell>' 123 + '<table:database-ranges><table:database-range table:name="DB1" table:target-range-address="Sheet1.A1:Sheet1.A3">' 124 '<table:filter><table:filter-and>${f}</table:filter-and></table:filter></table:database-range></table:database-ranges>', 125 fields={"f": '<table:filter-condition table:field-number="0" table:operator="=" table:data-type="background-color" table:value="#ff0000"/>' 126 '<table:filter-condition table:field-number="0" table:operator="=" table:data-type="text-color" table:value="#0000ff"/>' 127 '<table:filter-condition table:field-number="0" table:operator="!=" table:value="b"/>'}, 128 base={"f": '<table:filter-condition table:field-number="0" table:operator="!=" table:value="b"/>'}), 129 dict(name="chart-major-origin", ext="odc", construct='chart:major-origin and chart:minor-logarithmic on style:chart-properties', 130 auto='<style:style style:name="ch1" style:family="chart"><style:chart-properties ${a}/></style:style>', 131 content='<chart:chart chart:class="chart:bar"><chart:plot-area><chart:axis chart:dimension="y" chart:name="primary-y" chart:style-name="ch1"/></chart:plot-area></chart:chart>', 132 fields={"a": 'chart:major-origin="0" chart:minor-logarithmic="true"'}, base={}), 133 dict(name="allow-overlap-decorative", ext="odt", construct='draw:allow-overlap and draw:decorative on style:graphic-properties', 134 auto=GSTYLE % ("G1", '<style:graphic-properties ${a}/>'), content='<text:p>' + FRAME % (' draw:style-name="G1"', "") + '</text:p>', 135 fields={"a": 'draw:allow-overlap="true" draw:decorative="true"'}, base={}), 136 dict(name="margin-gutter", ext="odt", construct='style:margin-gutter on style:page-layout-properties', 137 content=TXT, fields={"pl": 'fo:margin-left="2cm" style:margin-gutter="1cm"'}, base={"pl": 'fo:margin-left="2cm"'}), 138 dict(name="relative-tab-stop-position", ext="odt", construct='text:relative-tab-stop-position on style:paragraph-properties', 139 auto=pstyle("P1", '<style:paragraph-properties ${a}><style:tab-stops><style:tab-stop style:position="5cm"/></style:tab-stops></style:paragraph-properties>'), 140 content='<text:p text:style-name="P1">A<text:tab/>B</text:p>', fields={"a": 'text:relative-tab-stop-position="false"'}, base={}), 141 dict(name="start-value-zero", ext="odt", construct='text:start-value="0" on text:list-level-style-number', 142 auto='<text:list-style style:name="L1"><text:list-level-style-number text:level="1" style:num-format="1" text:start-value="${v}"/></text:list-style>', 143 content='<text:list text:style-name="L1"><text:list-item><text:p>Zero</text:p></text:list-item><text:list-item><text:p>One</text:p></text:list-item></text:list>', 144 fields={"v": "0"}, base={"v": "1"}), 145 dict(name="handle-new-attributes", ext="odg", construct='draw:handle with only draw:handle-position-x/-y, and a polar one with -polar-pole-x/-y, -polar-radius/-angle', 146 content=GEOM, fields={"eg": "", "handles": '<draw:handle draw:handle-position-x="10800" draw:handle-position-y="0"/>' 147 '<draw:handle draw:handle-polar-pole-x="10800" draw:handle-polar-pole-y="10800" draw:handle-polar-radius="5000" draw:handle-polar-angle="0"/>'}, 148 base={"handles": ""}), 149] 150 151 152def build(case, base): 153 mime, body, wrap, paged = KINDS[case["ext"]] 154 f = {k: "" for k in case["fields"]} 155 f.update(case["base"] if base else case["fields"]) 156 f = {k: Template(v).substitute(f) for k, v in f.items()} # fields may refer to other fields, one level 157 sub = lambda s: Template(s).substitute(f) 158 content = sub(Template(wrap).substitute(content=case["content"])) 159 head = '<?xml version="1.0" encoding="UTF-8"?>\n<%s ' + NSDECL + ' office:version="1.4">%s</%s>\n' 160 cbody = '<office:automatic-styles>%s</office:automatic-styles><office:body><%s>%s</%s></office:body>' % (sub(case.get("auto", "")), body, content, body) 161 sbody = '<office:styles>%s</office:styles>' % sub(case.get("styles", "")) 162 if paged: 163 sbody += ('<office:automatic-styles><style:page-layout style:name="PM1"><style:page-layout-properties %s/></style:page-layout></office:automatic-styles>' 164 '<office:master-styles><style:master-page style:name="Default" style:page-layout-name="PM1"/></office:master-styles>') % f.get("pl", "") 165 streams = {"content.xml": head % ("office:document-content", cbody, "office:document-content"), 166 "styles.xml": head % ("office:document-styles", sbody, "office:document-styles")} 167 entries = "".join('<manifest:file-entry manifest:full-path="%s" manifest:media-type="%s"/>' % (n, "image/png" if n.endswith(".png") else "text/xml") 168 for n in list(streams) + list(case.get("extra", {}))) 169 mani = ('<?xml version="1.0" encoding="UTF-8"?>\n<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.4">' 170 '<manifest:file-entry manifest:full-path="/" manifest:version="1.4" manifest:media-type="%s"/>%s</manifest:manifest>\n') % (mime, entries) 171 path = os.path.join(OUT, case["name"] + ("-base" if base else "") + "." + case["ext"]) 172 with zipfile.ZipFile(path, "w") as z: 173 def put(n, data, comp): 174 z.writestr(zipfile.ZipInfo(n, (2026, 11, 3, 0, 0, 0)), data, compress_type=comp) 175 put("mimetype", mime, zipfile.ZIP_STORED) 176 for n, t in streams.items(): 177 put(n, t, zipfile.ZIP_DEFLATED) 178 for n, d in case.get("extra", {}).items(): 179 put(n, d, zipfile.ZIP_STORED) 180 put("META-INF/manifest.xml", mani, zipfile.ZIP_DEFLATED) 181 return path 182 183 184def report(valjson): 185 val = json.load(open(valjson)) 186 count = {v: {} for v in ("13", "14")} 187 for v in count: 188 for e in val["errors"][v]: 189 # validate.py only rewrites a 1.2/1.3 version to the target, so every 1.4 file draws its three version errors under 1.3 190 if v == "13" and e[3].startswith(('value of attribute "office:version"', 'value of attribute "manifest:version"')): 191 continue 192 count[v][e[0].split("/")[0]] = count[v].get(e[0].split("/")[0], 0) + 1 193 rows = [] 194 for c in CASES: 195 for base in (False, True): 196 if base and c["base"] is None: 197 continue 198 n = c["name"] + ("-base" if base else "") + "." + c["ext"] 199 if n in val["packages"]: 200 rows.append((n, "(control without the construct)" if base else c["construct"], count["14"].get(n, 0), count["13"].get(n, 0))) 201 else: 202 rows.append((n, "NOT VALIDATED", -1, -1)) 203 w = max(len(r[0]) for r in rows) 204 print("%-*s %4s %4s construct (1.3 count leaves out the office:version/manifest:version errors)" % (w, "package", "1.4", "1.3")) 205 for n, k, e14, e13 in rows: 206 print("%-*s %4d %4d %s" % (w, n, e14, e13, k)) 207 208 209if len(sys.argv) == 3 and sys.argv[1] == "--report": 210 report(sys.argv[2]) 211else: 212 os.makedirs(OUT, exist_ok=True) 213 for c in CASES: 214 print(build(c, False)) 215 if c["base"] is not None: 216 print(build(c, True)) 217