xref: /trunk/test/odf-validate/dump7.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
1# usage: runpy.sh dump7.py <port> <outdir> <file>...
2# Loads each ODF package read-only over UNO and writes a JSON dump of a fixed property set (Task 7, Review Focus 2).
3import sys, os, json, hashlib, uno
4from unohelp import connect, desktop, load
5
6port, outdir, files = int(sys.argv[1]), sys.argv[2], sys.argv[3:]
7LIM = 400
8
9def v(x, depth=0):
10    if isinstance(x, uno.Enum): return x.value
11    if x is None or isinstance(x, (bool, int, float, str)): return x
12    if isinstance(x, (tuple, list)): return [v(i, depth + 1) for i in x][:200]
13    if hasattr(x, "typeName"):
14        if depth > 3: return "<deep>"
15        try: return {k: v(getattr(x, k), depth + 1) for k in dir(x) if not k.startswith("_") and k not in ("typeName", "value")}
16        except Exception: return "<struct>"
17    return "<obj>"
18def g(o, n):
19    try: return v(o.getPropertyValue(n))
20    except Exception: return "n/a"
21def gs(o, names): return [g(o, n) for n in names]
22def h(s): return hashlib.sha256(s.encode("utf-8", "replace")).hexdigest()[:16]
23def tryf(f, d="n/a"):
24    try: return f()
25    except Exception as e: return d if d != "ERR" else repr(e)[:80]
26def has(doc, s): return tryf(lambda: doc.supportsService(s), False)
27
28SHAPE_PROPS = ["FillStyle", "FillColor", "FillTransparence", "FillGradientName", "FillHatchName", "FillBitmapName", "LineStyle", "LineColor", "LineWidth", "CharHeight", "ParaAdjust"]
29def shape(s, depth=0):
30    d = {"t": tryf(lambda: s.ShapeType), "name": tryf(lambda: s.Name), "pos": v(tryf(lambda: s.Position)), "size": v(tryf(lambda: s.Size)),
31         "text": tryf(lambda: s.String[:80]), "p": gs(s, SHAPE_PROPS)}
32    t = d["t"] if isinstance(d["t"], str) else ""
33    if "Connector" in t:
34        d["conn"] = {"kind": g(s, "EdgeKind"), "start": g(s, "StartPosition"), "end": g(s, "EndPosition"), "bez": g(s, "PolyPolygonBezier")}
35    if "OLE2" in t:
36        d["ole"] = {"cls": g(s, "CLSID"), "internal": g(s, "IsInternal"), "model": embedded(tryf(lambda: s.Model, None))}
37    if "GraphicObject" in t: d["gr"] = gs(s, ["GraphicURL", "Transparency"])[1:]
38    if tryf(lambda: s.supportsService("com.sun.star.drawing.GroupShape"), False) and depth < 4:
39        d["kids"] = [shape(s.getByIndex(i), depth + 1) for i in range(min(s.Count, 100))]
40    return d
41def forms_of(page):
42    out = []
43    try: forms = page.Forms
44    except Exception: return out
45    def walk(c):
46        for i in range(min(c.Count, 300)):
47            e = c.getByIndex(i)
48            if tryf(lambda: e.supportsService("com.sun.star.form.FormComponents"), False) or tryf(lambda: e.supportsService("com.sun.star.form.component.Form"), False):
49                out.append(["FORM", tryf(lambda: e.Name)]); walk(e)
50            else:
51                names = tryf(lambda: [p.Name for p in e.getPropertySetInfo().getProperties()], [])
52                out.append([tryf(lambda: e.Name), tryf(lambda: e.ImplementationName), g(e, "ObjIDinMSO") if "ObjIDinMSO" in names else "-",
53                            tryf(lambda: e.getValueBinding() is not None, "-"), tryf(lambda: (e.getListEntrySource() is not None), "-"), len(names)])
54    walk(forms)
55    return out
56def chart_info(m):
57    if m is None: return None
58    try:
59        r = {"title": tryf(lambda: m.Title.String if m.HasMainTitle else ""), "diagram": tryf(lambda: m.Diagram.ServiceName if hasattr(m.Diagram, "ServiceName") else m.getDiagram().getDiagramType()),
60             "addshapes": tryf(lambda: [m.DrawPage.Count, sorted(m.DrawPage.getByIndex(i).ShapeType.split(".")[-1] for i in range(m.DrawPage.Count))]), "intdata": tryf(lambda: m.hasInternalDataProvider())}
61        return r
62    except Exception: return "n/a"
63def embedded(m, depth=0):
64    """Dump an embedded model: drawing and spreadsheet documents fully, charts by title/type/additional shapes."""
65    if m is None: return None
66    if depth > 1: return "<deep>"
67    try:
68        if has(m, "com.sun.star.drawing.DrawingDocument") or has(m, "com.sun.star.presentation.PresentationDocument"): return {"draw": draw(m)}
69        if has(m, "com.sun.star.sheet.SpreadsheetDocument"): return {"calc": calc(m)}
70        if has(m, "com.sun.star.chart2.ChartDocument") or has(m, "com.sun.star.chart.ChartDocument"): return chart_info(m)
71        return {"other": tryf(lambda: list(m.SupportedServiceNames)[:3])}
72    except Exception as e: return "ERR " + repr(e)[:80]
73def docprops(doc):
74    p = doc.DocumentProperties
75    ud = tryf(lambda: [[n, v(p.UserDefinedProperties.getPropertyValue(n))] for n in sorted(i.Name for i in p.UserDefinedProperties.getPropertySetInfo().getProperties())], [])
76    return {"title": p.Title, "subject": p.Subject, "author": p.Author, "keywords": list(p.Keywords), "desc": p.Description, "gen": p.Generator[:20] if p.Generator else "",
77            "print_date": v(p.PrintDate), "printed_by": p.PrintedBy, "creation_date": v(p.CreationDate), "template": p.TemplateName, "edit_cycles": p.EditingCycles,
78            "autoload": [p.AutoloadURL, p.AutoloadSecs], "defaulttarget": p.DefaultTarget, "userdef": ud}
79def basic(doc):
80    out = {}
81    for k in ("BasicLibraries", "DialogLibraries"):
82        out[k] = tryf(lambda: sorted(getattr(doc, k).ElementNames), "n/a")
83    return out
84
85def writer(doc):
86    o = {}
87    txt = doc.Text.String
88    o["text_hash"] = h(txt); o["chars"] = len(txt)
89    paras = []; ncell = 0
90    e = doc.Text.createEnumeration(); tbl = 0; port_hash = []
91    while e.hasMoreElements() and len(paras) < LIM:
92        p = e.nextElement()
93        if p.supportsService("com.sun.star.text.Paragraph"):
94            paras.append([len(p.String), g(p, "ParaStyleName"), g(p, "ParaBackColor"), g(p, "ParaBackTransparent"), g(p, "FillStyle"), g(p, "FillColor"), g(p, "FillTransparence"),
95                          g(p, "CharHeight"), g(p, "ParaAdjust"), g(p, "ParaHyphenationMaxLeadingChars"), g(p, "ParaHyphenationMaxTrailingChars"), g(p, "ParaHyphenationMaxHyphens")])
96            try:
97                pe = p.createEnumeration(); sig = []
98                while pe.hasMoreElements():
99                    t = pe.nextElement(); ty = t.TextPortionType; sig.append(ty)
100                    if ty == "TextField": sig.append(tryf(lambda: t.TextField.ServiceName if hasattr(t.TextField, "ServiceName") else ",".join(t.TextField.SupportedServiceNames)))
101                    u = g(t, "HyperLinkURL")
102                    if u: sig.append("URL:" + str(u))
103                port_hash.append(h("|".join(map(str, sig))))
104            except Exception: port_hash.append("n/a")
105        else: tbl += 1
106    o["paras"] = paras; o["nparas"] = len(paras); o["port_hash"] = h("".join(port_hash)); o["top_tables"] = tbl
107    ps = doc.StyleFamilies.getByName("PageStyles")
108    pk = ["Width", "Height", "BackColor", "BackTransparent", "FillStyle", "FillColor", "FillTransparence", "HeaderIsOn", "HeaderBackColor", "HeaderBackTransparent", "HeaderFillStyle",
109          "HeaderFillColor", "HeaderFillTransparence", "FooterIsOn", "FooterBackColor", "FooterBackTransparent", "FooterFillStyle", "FooterFillColor", "FooterFillTransparence",
110          "GridMode", "GridBaseWidth", "GridBaseHeight", "GridLines", "GridSnapToChars", "StandardPageMode", "GridRubyHeight", "GridDisplay", "GridPrint", "IsLandscape", "PageStyleLayout"]
111    o["pagestyles"] = {n: gs(ps.getByName(n), pk) + [bool(g(ps.getByName(n), "BackGraphicURL")), bool(g(ps.getByName(n), "HeaderBackGraphicURL")), bool(g(ps.getByName(n), "FooterBackGraphicURL"))] for n in ps.ElementNames}
112    pa = doc.StyleFamilies.getByName("ParagraphStyles")
113    ak = ["ParaBackColor", "ParaBackTransparent", "FillStyle", "FillColor", "FillTransparence", "CharHeight", "ParaHyphenationMaxLeadingChars", "ParaHyphenationMaxTrailingChars", "ParaKeepTogether", "ParaAdjust"]
114    o["parastyles"] = {n: gs(pa.getByName(n), ak) for n in pa.ElementNames}
115    cs = doc.StyleFamilies.getByName("CharacterStyles")
116    o["charstyles"] = {n: gs(cs.getByName(n), ["CharHeight", "CharWeight", "CharColor"]) for n in cs.ElementNames}
117    fs = doc.StyleFamilies.getByName("FrameStyles")
118    o["framestyles"] = {n: gs(fs.getByName(n), ["BackColor", "FillStyle", "FillTransparence", "FillColor", "Width", "Height"]) for n in fs.ElementNames}
119    tabs = []
120    for i in range(min(doc.TextTables.Count, 100)):
121        t = doc.TextTables.getByIndex(i)
122        cn = list(t.CellNames)
123        tabs.append({"name": t.Name, "rows": t.Rows.Count, "cols": t.Columns.Count, "cells": len(cn), "cellhash": h("|".join(cn) + "".join(tryf(lambda: t.getCellByName(c).String, "") for c in cn[:300])),
124                     "split": [tryf(lambda: t.Rows.getByIndex(r).IsSplitAllowed) for r in range(min(t.Rows.Count, 50))], "bg": g(t, "BackColor"), "kt": g(t, "Split")})
125    o["tables"] = tabs
126    fld = []
127    en = doc.TextFields.createEnumeration()
128    while en.hasMoreElements() and len(fld) < LIM:
129        f = en.nextElement()
130        sn = tryf(lambda: ",".join(f.SupportedServiceNames))
131        r = [sn.replace("com.sun.star.text.", "")[:120]]
132        if "GetReference" in sn: r += gs(f, ["ReferenceFieldPart", "ReferenceFieldSource", "SourceName", "SequenceNumber"])
133        if "URL" in sn: r += gs(f, ["URL", "Representation"])
134        fld.append(r)
135    o["fields"] = fld
136    o["footnotes"] = [doc.Footnotes.Count, [tryf(lambda: doc.Footnotes.getByIndex(i).String[:30], "") for i in range(min(doc.Footnotes.Count, 30))]]
137    o["endnotes"] = [doc.Endnotes.Count, [tryf(lambda: doc.Endnotes.getByIndex(i).String[:30], "") for i in range(min(doc.Endnotes.Count, 30))]]
138    fr = []
139    for coll, name in ((doc.TextFrames, "frame"), (doc.GraphicObjects, "graphic"), (doc.EmbeddedObjects, "embedded")):
140        for n in coll.ElementNames[:100]:
141            f = coll.getByName(n)
142            fr.append([name, n, g(f, "AnchorType"), g(f, "AnchorPageNo"), g(f, "Width"), g(f, "Height"), g(f, "BackColor"), g(f, "FillTransparence"), g(f, "Transparency"), g(f, "FillStyle"), g(f, "HoriOrientPosition"), g(f, "VertOrientPosition")])
143            if name == "embedded": fr[-1].append(embedded(tryf(lambda: f.Model, None)))
144    o["frames"] = fr
145    sec = []
146    for n in doc.TextSections.ElementNames[:100]:
147        s = doc.TextSections.getByName(n); sec.append([n, g(s, "FileLink"), g(s, "LinkRegion"), g(s, "IsProtected"), g(s, "IsVisible")])
148    o["sections"] = sec
149    rl = []
150    try:
151        en = doc.Redlines.createEnumeration()
152        while en.hasMoreElements() and len(rl) < 100:
153            r = en.nextElement(); rl.append([g(r, "RedlineType"), g(r, "RedlineAuthor"), g(r, "RedlineDateTime"), g(r, "RedlineComment"), g(r, "RedlineSuccessorData") != "n/a" and bool(g(r, "RedlineSuccessorData"))])
154    except Exception: rl = "n/a"
155    o["redlines"] = rl
156    o["bookmarks"] = tryf(lambda: sorted(doc.Bookmarks.ElementNames))
157    o["refmarks"] = tryf(lambda: sorted(doc.ReferenceMarks.ElementNames))
158    o["indexes"] = tryf(lambda: doc.DocumentIndexes.Count)
159    o["shapes"] = [shape(doc.DrawPage.getByIndex(i)) for i in range(min(doc.DrawPage.Count, 150))]
160    o["forms"] = forms_of(doc.DrawPage)
161    o["nstyles"] = {k: len(doc.StyleFamilies.getByName(k).ElementNames) for k in doc.StyleFamilies.ElementNames}
162    return o
163
164def calc(doc):
165    o = {"sheets": []}
166    nfs = doc.NumberFormats; ftype = {}
167    def ft(k):
168        if k not in ftype: ftype[k] = tryf(lambda: nfs.getByKey(k).Type)
169        return ftype[k]
170    for i in range(doc.Sheets.Count):
171        sh = doc.Sheets.getByIndex(i)
172        cur = sh.createCursor(); cur.gotoEndOfUsedArea(False)
173        rng = sh.getCellRangeByPosition(0, 0, cur.RangeAddress.EndColumn, cur.RangeAddress.EndRow)
174        if cur.RangeAddress.EndColumn > 300 or cur.RangeAddress.EndRow > 3000:
175            rng = sh.getCellRangeByPosition(0, 0, min(cur.RangeAddress.EndColumn, 300), min(cur.RangeAddress.EndRow, 3000))
176        da = rng.DataArray; fa = rng.FormulaArray
177        so = {"name": sh.Name, "end": [cur.RangeAddress.EndColumn, cur.RangeAddress.EndRow], "data_hash": h(repr(da)), "formula_hash": h(repr(fa)),
178              "nformulas": sum(1 for r in fa for c in r if isinstance(c, str) and c.startswith("="))}
179        cells = []
180        nrow = min(len(da), 400)
181        for r in range(nrow):
182            for c in range(min(len(da[r]), 60)):
183                if da[r][c] != "" and da[r][c] is not None:
184                    cell = rng.getCellByPosition(c, r)
185                    cells.append([r, c, ft(cell.NumberFormat), cell.getError(), cell.Type.value])
186        so["cells_hash"] = h(repr(cells)); so["ncells"] = len(cells)
187        so["errors"] = sum(1 for x in cells if x[3])
188        so["annotations"] = [sh.Annotations.Count, h("".join(tryf(lambda: sh.Annotations.getByIndex(k).String, "") for k in range(min(sh.Annotations.Count, 100))))]
189        so["charts"] = {n: chart_info(tryf(lambda: sh.Charts.getByName(n).EmbeddedObject, None)) for n in sh.Charts.ElementNames[:30]}
190        urls = []
191        for r in range(nrow):
192            for c in range(min(len(da[r]), 40)):
193                if isinstance(fa[r][c], str) and fa[r][c] and not fa[r][c].startswith("="):
194                    cell = rng.getCellByPosition(c, r)
195                    try:
196                        en = cell.Text.createEnumeration()
197                        while en.hasMoreElements():
198                            pe = en.nextElement().createEnumeration()
199                            while pe.hasMoreElements():
200                                pt = pe.nextElement()
201                                if pt.TextPortionType == "TextField": urls.append([r, c, tryf(lambda: pt.TextField.URL), tryf(lambda: pt.TextField.Representation)])
202                    except Exception: pass
203                    if len(urls) > 100: break
204        so["urls"] = urls
205        so["shapes"] = [shape(sh.DrawPage.getByIndex(k)) for k in range(min(sh.DrawPage.Count, 100))]
206        so["forms"] = forms_of(sh.DrawPage)
207        so["pagestyle"] = sh.PageStyle
208        so["flags"] = gs(sh, ["IsVisible", "IsProtected"])
209        o["sheets"].append(so)
210    o["dde"] = tryf(lambda: [[n, doc.DDELinks.getByName(n).Application, doc.DDELinks.getByName(n).Topic, doc.DDELinks.getByName(n).Item] for n in doc.DDELinks.ElementNames])
211    o["names"] = tryf(lambda: sorted(doc.NamedRanges.ElementNames))
212    ps = doc.StyleFamilies.getByName("PageStyles")
213    o["pagestyles"] = {n: gs(ps.getByName(n), ["BackColor", "BackTransparent", "FillStyle", "HeaderIsOn", "FooterIsOn", "IsLandscape", "Width", "Height", "ScaleToPagesX"]) for n in ps.ElementNames}
214    cs = doc.StyleFamilies.getByName("CellStyles")
215    o["cellstyles"] = {n: gs(cs.getByName(n), ["CellBackColor", "IsCellBackgroundTransparent", "CharHeight", "NumberFormat"]) for n in cs.ElementNames}
216    return o
217
218def draw(doc):
219    o = {"pages": []}
220    pages = doc.DrawPages
221    for i in range(pages.Count):
222        pg = pages.getByIndex(i)
223        po = {"name": tryf(lambda: pg.Name), "shapes": [shape(pg.getByIndex(k)) for k in range(min(pg.Count, 300))], "forms": forms_of(pg),
224              "layout": g(pg, "Layout"), "bg": tryf(lambda: gs(pg.Background, ["FillStyle", "FillColor", "FillTransparence", "FillGradientName"])), "master": tryf(lambda: pg.MasterPage.Name)}
225        if has(doc, "com.sun.star.presentation.PresentationDocument"):
226            po["notes"] = tryf(lambda: [pg.NotesPage.getByIndex(k).String[:40] for k in range(min(pg.NotesPage.Count, 20)) if hasattr(pg.NotesPage.getByIndex(k), "String")], "n/a")
227        o["pages"].append(po)
228    masters = doc.MasterPages
229    o["masters"] = [[tryf(lambda: masters.getByIndex(i).Name), len(shape_list(masters.getByIndex(i)))] for i in range(masters.Count)]
230    for fam in ("graphics", "Default"):
231        pass
232    try:
233        gf = doc.StyleFamilies.getByName("graphics")
234        o["graphicstyles"] = {n: gs(gf.getByName(n), ["FillStyle", "FillColor", "FillTransparence", "CharHeight", "LineStyle", "LineColor", "TextWritingMode"]) for n in gf.ElementNames}
235    except Exception: pass
236    return o
237def shape_list(pg): return [shape(pg.getByIndex(k)) for k in range(min(pg.Count, 300))]
238
239DESK = desktop(connect(port))
240os.makedirs(outdir, exist_ok=True)
241for f in files:
242    dest = os.path.join(outdir, os.path.basename(f) + ".json")
243    if os.path.exists(dest): continue
244    open(os.path.join(outdir, ".current"), "w").write(f)
245    out = {}
246    try:
247        doc = load(DESK, f)
248        out["type"] = ("writer" if has(doc, "com.sun.star.text.TextDocument") else "calc" if has(doc, "com.sun.star.sheet.SpreadsheetDocument")
249                       else "impress" if has(doc, "com.sun.star.presentation.PresentationDocument") else "draw" if has(doc, "com.sun.star.drawing.DrawingDocument") else "other")
250        out["props"] = tryf(lambda: docprops(doc), "ERR")
251        out["basic"] = basic(doc)
252        try:
253            out["body"] = {"writer": writer, "calc": calc, "impress": draw, "draw": draw}.get(out["type"], lambda d: {})(doc)
254        except Exception as e:
255            out["body_error"] = repr(e)[:200]
256        doc.close(True)
257    except Exception as e:
258        out["error"] = repr(e)[:200]
259    json.dump(out, open(dest, "w"), sort_keys=True, indent=0, default=str)
260print("done", len(files))
261