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