diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4e1ba3c4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.FCStd diff=fcinfo diff --git a/fcinfo b/fcinfo new file mode 100755 index 00000000..79fda42a --- /dev/null +++ b/fcinfo @@ -0,0 +1,293 @@ +#!/usr/bin/python3 + +# -*- coding: utf8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2015 Yorik van Havre * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__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. + -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): # short: 0=normal, 1=short, 2=veryshort + + 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: + r = self.zfile.read(attributes["file"]) + s = r.__sizeof__() + if s < 1024: + s = str(s) + "B" + elif s > 1048576: + s = str(s / 1048576) + "M" + else: + s = str(s / 1024) + "K" + s += " " + str(hashlib.sha1(r).hexdigest()[:12]) + self.contents[self.obj] += " (" + s + ")" + + elif tag == "Property": + self.prop = None + # skip "internal" properties, useless for a diff + 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": + self.contents[self.obj + "00000000::" + self.prop] = str( + float(attributes["value"]) + ) + else: + self.contents[self.obj + "00000000::" + self.prop] = attributes["value"] + + elif tag in ["PropertyVector"]: + if self.prop and self.obj and (self.short == 0): + val = ( + "(" + + str(float(attributes["valueX"])) + + "," + + str(float(attributes["valueY"])) + + "," + + str(float(attributes["valueZ"])) + + ")" + ) + self.contents[self.obj + "00000000::" + self.prop] = val + + elif tag in ["PropertyPlacement"]: + if self.prop and self.obj and (self.short == 0): + val = ( + "(" + + str(float(attributes["Px"])) + + "," + + str(float(attributes["Py"])) + + "," + + str(float(attributes["Pz"])) + + ")" + ) + val += ( + " (" + + str(round(float(attributes["Q0"]), 4)) + + "," + + str(round(float(attributes["Q1"]), 4)) + + "," + ) + val += ( + str(round(float(attributes["Q2"]), 4)) + + "," + + str(round(float(attributes["Q3"]), 4)) + + ")" + ) + 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 = float((c >> 24) & 0xFF) / 255.0 + g = float((c >> 16) & 0xFF) / 255.0 + b = float((c >> 8) & 0xFF) / 255.0 + val = str((r, g, b)) + self.contents[self.obj + "00000000::" + self.prop] = val + + elif tag == "Objects": + self.count = attributes["Count"] + self.obj = None + + # Print all the contents of the document properties + items = self.contents.items() + items = sorted(items) + for key, value in items: + key = self.clean(key) + value = self.clean(value) + print(" " + key + " : " + value) + print(" 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(" " + 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 ("-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) + # a valid xml doc can have only one root element. So we need to insert + # all the contents of the GUiDocument tag into the main one + doc = re.sub(b"<\/Document>", b"", doc, flags=re.MULTILINE | re.DOTALL) + guidoc = re.sub(b"", 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))