Running this command will enable fcinfo in the local repository (`.git/config` file): git config --local diff.fcinfo.textconv "fcinfo" Also specify file extension on `.gitattributes` file Wiki page: https://wiki.freecad.org/WebTools_Git#Description
293 lines
11 KiB
Python
Executable file
293 lines
11 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
# -*- coding: utf8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * 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 <document> tag into the main one
|
|
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))
|