xref: /trunk/test/odf-validate/fixtures/t14/make-consumer.py (revision 837a267461b344831ad405991aa79c1adf3414dd)
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