| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __title__ = "FreeCAD File info utility" |
| | __author__ = "Yorik van Havre" |
| | __url__ = ["https://www.freecad.org"] |
| | __doc__ = """ |
| | This utility prints information about a given FreeCAD file (*.FCStd) |
| | on screen, including document properties, number of included objects, |
| | object sizes and properties and values. Its main use is to compare |
| | two files and be able to see the differences in a text-based form. |
| | |
| | If no option is used, fcinfo prints the document properties and a list |
| | of properties of each object found in the given file. |
| | |
| | Usage: |
| | |
| | fcinfo [options] myfile.FCStd |
| | |
| | Options: |
| | |
| | -h, --help: Prints this help text |
| | -s, --short: Do not print object properties. Only one line |
| | per object is printed, including its size and SHA1. |
| | This is sufficient to see that an object has |
| | changed, but not what exactly has changed. |
| | -vs --veryshort: Only prints the document info, not objects info. |
| | This is sufficient to see if a file has changed, as |
| | its SHA1 code and timestamp will show it. But won't |
| | show details of what has changed. |
| | -p --partinfo: Print size and hash of internal BREP part files |
| | -g --gui: Adds visual properties too (if not using -s or -vs) |
| | |
| | Git usage: |
| | |
| | This script can be used as a textconv tool for git diff by |
| | configuring your git folder as follows: |
| | |
| | 1) add to .gitattributes (or ~/.gitattributes for user-wide): |
| | |
| | *.fcstd diff=fcinfo |
| | |
| | 2) add to .git/config (or ~/.gitconfig for user-wide): |
| | |
| | [diff "fcinfo"] |
| | textconv = /path/to/fcinfo |
| | |
| | With this, when committing a .FCStd file with Git, |
| | 'git diff' will show you the difference between the two |
| | texts obtained by fcinfo |
| | """ |
| |
|
| |
|
| | import sys |
| | import zipfile |
| | import xml.sax |
| | import os |
| | import hashlib |
| | import re |
| |
|
| |
|
| | class FreeCADFileHandler(xml.sax.ContentHandler): |
| | def __init__(self, zfile, short=0): |
| |
|
| | xml.sax.ContentHandler.__init__(self) |
| | self.zfile = zfile |
| | self.obj = None |
| | self.prop = None |
| | self.count = "0" |
| | self.contents = {} |
| | self.short = short |
| |
|
| | def startElement(self, tag, attributes): |
| |
|
| | if tag == "Document": |
| | self.obj = tag |
| | self.contents = {} |
| | self.contents["ProgramVersion"] = attributes["ProgramVersion"] |
| | self.contents["FileVersion"] = attributes["FileVersion"] |
| |
|
| | elif tag == "Object": |
| | if "name" in attributes: |
| | name = self.clean(attributes["name"]) |
| | self.obj = name |
| | if "type" in attributes: |
| | self.contents[name] = attributes["type"] |
| |
|
| | elif tag == "ViewProvider": |
| | if "name" in attributes: |
| | self.obj = self.clean(attributes["name"]) |
| |
|
| | elif tag == "Part": |
| | if self.obj and partinfo: |
| | file = self.clean(attributes["file"]) |
| | r = self.zfile.read(attributes["file"]) |
| | s = r.__sizeof__() |
| | if s < 1024: |
| | s = f"{s:g}B" |
| | elif s > 1048576: |
| | s = f"{s / 1048576:.3g}M" |
| | else: |
| | s = f"{s / 1024:.3g}K" |
| | d = str(hashlib.sha1(r).hexdigest()[:12]) |
| | self.contents[self.obj] += f" ({file},{s},{d})" |
| |
|
| | elif tag == "Property": |
| | self.prop = None |
| | |
| | if attributes["name"] not in [ |
| | "Symbol", |
| | "AttacherType", |
| | "MapMode", |
| | "MapPathParameter", |
| | "MapReversed", |
| | "AttachmentOffset", |
| | "SelectionStyle", |
| | "TightGrid", |
| | "GridSize", |
| | "GridSnap", |
| | "GridStyle", |
| | "Lighting", |
| | "Deviation", |
| | "AngularDeflection", |
| | "BoundingBox", |
| | "Selectable", |
| | "ShowGrid", |
| | ]: |
| | self.prop = attributes["name"] |
| |
|
| | elif tag in ["String", "Uuid", "Float", "Integer", "Bool", "Link"]: |
| | if self.prop and ("value" in attributes): |
| | if self.obj == "Document": |
| | self.contents[self.prop] = attributes["value"] |
| | elif self.short == 0: |
| | if tag == "Float": |
| | val = float(attributes["value"]) |
| | self.contents[self.obj + "00000000::" + self.prop] = f"{val:g}" |
| | else: |
| | self.contents[self.obj + "00000000::" + self.prop] = attributes["value"] |
| |
|
| | elif tag in ["PropertyVector"]: |
| | if self.prop and self.obj and (self.short == 0): |
| | vx = float(attributes["valueX"]) |
| | vy = float(attributes["valueY"]) |
| | vz = float(attributes["valueZ"]) |
| | val = f"({vx:g},{vy:g},{vz:g})" |
| | self.contents[self.obj + "00000000::" + self.prop] = val |
| |
|
| | elif tag in ["PropertyPlacement"]: |
| | if self.prop and self.obj and (self.short == 0): |
| | px = float(attributes["Px"]) |
| | py = float(attributes["Py"]) |
| | pz = float(attributes["Pz"]) |
| |
|
| | q0 = float(attributes["Q0"]) |
| | q1 = float(attributes["Q1"]) |
| | q2 = float(attributes["Q2"]) |
| | q3 = float(attributes["Q3"]) |
| |
|
| | val = f"({px:g},{py:g},{pz:g})" |
| | val += f"({q0:.3g},{q1:.3g},{q2:.3g},{q3:.3g})" |
| | self.contents[self.obj + "00000000::" + self.prop] = val |
| |
|
| | elif tag in ["PropertyColor"]: |
| | if self.prop and self.obj and (self.short == 0): |
| | c = int(attributes["value"]) |
| | r = (c >> 24) & 0xFF |
| | g = (c >> 16) & 0xFF |
| | b = (c >> 8) & 0xFF |
| | val = f"({r},{g},{b})" |
| | self.contents[self.obj + "00000000::" + self.prop] = val |
| |
|
| | elif tag == "Objects": |
| | self.count = attributes["Count"] |
| | self.obj = None |
| |
|
| | |
| | items = self.contents.items() |
| | items = sorted(items) |
| | for key, value in items: |
| | key = self.clean(key) |
| | value = self.clean(value) |
| | print(f" {key} : {value}") |
| | print(f" Objects: ({self.count})") |
| | self.contents = {} |
| |
|
| | def endElement(self, tag): |
| |
|
| | if (tag == "Document") and (self.short != 2): |
| | items = self.contents.items() |
| | items = sorted(items) |
| | for key, value in items: |
| | key = self.clean(key) |
| | if "00000000::" in key: |
| | key = " " + key.split("00000000::")[1] |
| | value = self.clean(value) |
| | if value: |
| | print(f" {key} : {value}") |
| |
|
| | def clean(self, value): |
| |
|
| | value = value.strip() |
| | return value |
| |
|
| |
|
| | if __name__ == "__main__": |
| |
|
| | if len(sys.argv) < 2: |
| | print(__doc__) |
| | sys.exit() |
| |
|
| | if ("-h" in sys.argv[1:]) or ("--help" in sys.argv[1:]): |
| | print(__doc__) |
| | sys.exit() |
| |
|
| | ext = sys.argv[-1].rsplit(".")[-1].lower() |
| | if not ext.startswith("fcstd") and not ext.startswith("fcbak"): |
| | print(__doc__) |
| | sys.exit() |
| |
|
| | if ("-vs" in sys.argv[1:]) or ("--veryshort" in sys.argv[1:]): |
| | short = 2 |
| | elif ("-s" in sys.argv[1:]) or ("--short" in sys.argv[1:]): |
| | short = 1 |
| | else: |
| | short = 0 |
| |
|
| | if ("-p" in sys.argv[1:]) or ("--partinfo" in sys.argv[1:]): |
| | partinfo = True |
| | else: |
| | partinfo = False |
| |
|
| | if ("-g" in sys.argv[1:]) or ("--gui" in sys.argv[1:]): |
| | gui = True |
| | else: |
| | gui = False |
| |
|
| | zfile = zipfile.ZipFile(sys.argv[-1]) |
| |
|
| | if not "Document.xml" in zfile.namelist(): |
| | sys.exit(1) |
| | doc = zfile.read("Document.xml") |
| | if gui and "GuiDocument.xml" in zfile.namelist(): |
| | guidoc = zfile.read("GuiDocument.xml") |
| | guidoc = re.sub(b"<\\?xml.*?-->", b" ", guidoc, flags=re.MULTILINE | re.DOTALL) |
| | |
| | |
| | doc = re.sub(b"<\\/Document>", b"", doc, flags=re.MULTILINE | re.DOTALL) |
| | guidoc = re.sub(b"<Document.*?>", b" ", guidoc, flags=re.MULTILINE | re.DOTALL) |
| | doc += guidoc |
| | s = os.path.getsize(sys.argv[-1]) |
| | if s < 1024: |
| | s = str(s) + "B" |
| | elif s > 1048576: |
| | s = str(s / 1048576) + "M" |
| | else: |
| | s = str(s / 1024) + "K" |
| | print("Document: " + sys.argv[-1] + " (" + s + ")") |
| | print(" SHA1: " + str(hashlib.sha1(open(sys.argv[-1], "rb").read()).hexdigest())) |
| | xml.sax.parseString(doc, FreeCADFileHandler(zfile, short)) |
| |
|