1+ //
2+ // Title: BIM Slimmer - strip metadata bloat to reduce file size
3+ //
4+ // Author: Alexis Olson using GPT-5 Thinking and Claude Opus 4.1
5+ //
6+ // Description:
7+ // Opens a Tabular model .bim file, removes UI/engine bloat while preserving model semantics,
8+ // and saves a sibling .slim JSON. Preserves structural containers (model, tables, columns,
9+ // measures, partitions, relationships, etc.), removes empty/unused values, and supports
10+ // optional switches for display/query group metadata. Shows a summary with items removed and
11+ // size savings.
12+ //
13+ // How to use:
14+ // - Use in Tabular Editor (2 or 3) Advanced Scripting.
15+ // - (Optional) Customize the configuration options at the top of the script.
16+ // - When prompted, select a <ModelName>.bim file.
17+ // - The script writes <ModelName>.slim and displays a summary.
18+
19+ using System ;
20+ using System . Collections . Generic ;
21+ using System . IO ;
22+ using System . Linq ;
23+ using System . Windows . Forms ;
24+ using Newtonsoft . Json ;
25+ using Newtonsoft . Json . Linq ;
26+
27+ // ==================== CONFIGURATION ====================
28+ // Use defaults specified or customize as needed
29+
30+ // Core metadata removal (recommended: ON)
31+ bool REMOVE_Annotations = true ; // annotations, changedProperties, extendedProperties
32+ bool REMOVE_Lineage = true ; // lineageTag, sourceLineageTag
33+ bool REMOVE_LanguageData = true ; // cultures, translations, synonyms, linguisticMetadata
34+
35+ // Value-based cleanup (recommended: ON)
36+ bool REMOVE_DefaultValues = true ; // dataCategory:Uncategorized, summarizeBy:none
37+ bool REMOVE_RedundantNames = true ; // sourceColumn==name, displayName==name
38+ bool REMOVE_EmptyContainers = true ; // empty {} and [] (preserves structural containers)
39+
40+ // Presentation properties (optional)
41+ bool REMOVE_SummarizeBy = true ; // summarizeBy (all values, not just none)
42+ bool REMOVE_DisplayProps = true ; // isHidden, displayFolder
43+ bool REMOVE_QueryGroups = false ; // queryGroup, queryGroups, folder
44+ bool REMOVE_FormatString = true ; // formatString literal only (NEVER formatStringDefinition)
45+
46+ // Additional metadata (recommended: ON)
47+ bool REMOVE_ExtraMetadata = true ; // sourceProviderType, isNameInferred, isDataTypeInferred
48+
49+ // ==================== OUTPUT FORMAT =====================
50+ // Human-friendly indented JSON (false) or compacted (true)
51+ bool MINIFY_OUTPUT = true ;
52+
53+ // ==================== MAIN EXECUTION ====================
54+ try
55+ {
56+ // Select file
57+ string inputPath ;
58+ using ( var dialog = new OpenFileDialog {
59+ Title = "Select BIM file to slim" ,
60+ Filter = "Tabular Model (*.bim)|*.bim|All files (*.*)|*.*" ,
61+ RestoreDirectory = true
62+ } ) {
63+ if ( dialog . ShowDialog ( ) != DialogResult . OK ) return ;
64+ inputPath = dialog . FileName ;
65+ }
66+
67+ // Generate output path
68+ var outputPath = Path . ChangeExtension ( inputPath , ".slim" ) ;
69+ var originalSize = new FileInfo ( inputPath ) . Length ;
70+
71+ // Parse JSON
72+ var root = JToken . Parse ( File . ReadAllText ( inputPath ) ) ;
73+
74+ // Build removal rules
75+ var dropKeys = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
76+ if ( REMOVE_Annotations ) {
77+ dropKeys . UnionWith ( new [ ] { "annotations" , "changedProperties" , "extendedProperties" } ) ;
78+ }
79+ if ( REMOVE_Lineage ) {
80+ dropKeys . UnionWith ( new [ ] { "lineageTag" , "sourceLineageTag" } ) ;
81+ }
82+ if ( REMOVE_LanguageData ) {
83+ dropKeys . UnionWith ( new [ ] { "cultures" , "translations" , "synonyms" , "linguisticMetadata" } ) ;
84+ }
85+ if ( REMOVE_SummarizeBy ) {
86+ dropKeys . Add ( "summarizeBy" ) ;
87+ }
88+ if ( REMOVE_DisplayProps ) {
89+ dropKeys . UnionWith ( new [ ] { "isHidden" , "displayFolder" } ) ;
90+ }
91+ if ( REMOVE_QueryGroups ) {
92+ dropKeys . UnionWith ( new [ ] { "queryGroup" , "queryGroups" , "folder" } ) ;
93+ }
94+ if ( REMOVE_ExtraMetadata ) {
95+ dropKeys . UnionWith ( new [ ] { "sourceProviderType" , "isNameInferred" , "isDataTypeInferred" } ) ;
96+ }
97+
98+ // Structural containers - never remove even if empty (preserves model schema)
99+ var preserve = new HashSet < string > ( new [ ] {
100+ "model" , "tables" , "columns" , "measures" , "relationships" , "partitions" ,
101+ "roles" , "hierarchies" , "levels" , "dataSources" , "perspectives" , "expressions"
102+ } , StringComparer . OrdinalIgnoreCase ) ;
103+
104+ // Track removals
105+ var stats = new Dictionary < string , int > ( ) ;
106+ Action < string > Track = delegate ( string key ) {
107+ stats [ key ] = stats . ContainsKey ( key ) ? stats [ key ] + 1 : 1 ;
108+ } ;
109+
110+ // Helpers
111+ Func < string , string , bool > Eq = delegate ( string a , string b ) {
112+ return string . Equals (
113+ a != null ? a . Trim ( ) : null ,
114+ b != null ? b . Trim ( ) : null
115+ ) ;
116+ } ;
117+
118+ Func < JToken , bool > IsEmpty = delegate ( JToken t ) {
119+ return t == null || t . Type == JTokenType . Null ||
120+ ( t is JContainer && ! ( ( JContainer ) t ) . HasValues ) ||
121+ ( t . Type == JTokenType . String && string . IsNullOrWhiteSpace ( ( string ) t ) ) ;
122+ } ;
123+
124+ // Recursive cleaner
125+ Action < JToken > Clean = null ;
126+ Clean = delegate ( JToken token ) {
127+ if ( token == null ) return ;
128+
129+ if ( token . Type == JTokenType . Object ) {
130+ var obj = ( JObject ) token ;
131+
132+ // Recurse first (depth-first)
133+ foreach ( var prop in obj . Properties ( ) . ToList ( ) ) Clean ( prop . Value ) ;
134+
135+ var toRemove = new List < JProperty > ( ) ;
136+
137+ foreach ( var prop in obj . Properties ( ) ) {
138+ // Name-based removals
139+ if ( dropKeys . Contains ( prop . Name ) ) {
140+ toRemove . Add ( prop ) ;
141+ Track ( prop . Name ) ;
142+ continue ;
143+ }
144+
145+ // formatString special handling (protect formatStringDefinition)
146+ if ( REMOVE_FormatString && Eq ( prop . Name , "formatString" ) ) {
147+ toRemove . Add ( prop ) ;
148+ Track ( "formatString" ) ;
149+ continue ;
150+ }
151+
152+ // Empty container removal (with structural preservation)
153+ if ( REMOVE_EmptyContainers && IsEmpty ( prop . Value ) && ! preserve . Contains ( prop . Name ) ) {
154+ toRemove . Add ( prop ) ;
155+ Track ( "empty" ) ;
156+ continue ;
157+ }
158+ }
159+
160+ // Value-based removals (checked after structure scan)
161+ if ( REMOVE_DefaultValues ) {
162+ var dc = obj . Property ( "dataCategory" ) ;
163+ if ( dc != null && dc . Value is JValue && Eq ( ( string ) dc . Value , "Uncategorized" ) ) {
164+ toRemove . Add ( dc ) ;
165+ Track ( "dataCategory=default" ) ;
166+ }
167+
168+ var sb = obj . Property ( "summarizeBy" ) ;
169+ if ( sb != null && sb . Value is JValue && Eq ( ( string ) sb . Value , "none" ) ) {
170+ toRemove . Add ( sb ) ;
171+ Track ( "summarizeBy=none" ) ;
172+ }
173+ }
174+
175+ if ( REMOVE_RedundantNames ) {
176+ var name = obj . Property ( "name" ) ;
177+ if ( name != null && name . Value is JValue ) {
178+ var nameStr = ( string ) name . Value ;
179+ var nameBracketed = nameStr != null ? string . Format ( "[{0}]" , nameStr ) : null ;
180+
181+ var src = obj . Property ( "sourceColumn" ) ;
182+ if (
183+ src != null &&
184+ src . Value is JValue &&
185+ (
186+ Eq ( ( string ) src . Value , nameStr ) ||
187+ Eq ( ( string ) src . Value , nameBracketed )
188+ )
189+ ) {
190+ toRemove . Add ( src ) ;
191+ Track ( "sourceColumn=name" ) ;
192+ }
193+
194+ var disp = obj . Property ( "displayName" ) ;
195+ if (
196+ disp != null &&
197+ disp . Value is JValue &&
198+ (
199+ Eq ( ( string ) disp . Value , nameStr ) ||
200+ Eq ( ( string ) disp . Value , nameBracketed )
201+ )
202+ ) {
203+ toRemove . Add ( disp ) ;
204+ Track ( "displayName=name" ) ;
205+ }
206+ }
207+ }
208+
209+ // Apply all removals
210+ foreach ( var prop in toRemove . Distinct ( ) ) prop . Remove ( ) ;
211+ }
212+ else if ( token . Type == JTokenType . Array ) {
213+ var arr = ( JArray ) token ;
214+ foreach ( var item in arr . ToList ( ) ) {
215+ Clean ( item ) ;
216+ if ( REMOVE_EmptyContainers && IsEmpty ( item ) ) {
217+ item . Remove ( ) ;
218+ Track ( "empty" ) ;
219+ }
220+ }
221+ }
222+ } ;
223+
224+ // Execute cleaning
225+ Clean ( root ) ;
226+
227+ // Save result
228+ var formatting = MINIFY_OUTPUT ? Formatting . None : Formatting . Indented ;
229+ File . WriteAllText ( outputPath , root . ToString ( formatting ) ) ;
230+
231+ // Report results
232+ var newSize = new FileInfo ( outputPath ) . Length ;
233+ var reduction = ( 1.0 - ( double ) newSize / originalSize ) * 100 ;
234+ var summary =
235+ "BIM Slimmer Results\n " +
236+ "==================\n " +
237+ string . Format ( "Input: {0} ({1:N1} KB)\n " , Path . GetFileName ( inputPath ) , originalSize / 1024.0 ) +
238+ string . Format ( "Output: {0} ({1:N1} KB)\n " , Path . GetFileName ( outputPath ) , newSize / 1024.0 ) +
239+ string . Format ( "Saved: {0:F1}%\n \n " , reduction ) +
240+ string . Format ( "Removed: {0:N0} items\n " , stats . Values . Sum ( ) ) +
241+ string . Join (
242+ "\n " ,
243+ stats . OrderBy ( k => k . Key )
244+ . Select ( k => string . Format ( " • {0}: {1:N0}" , k . Key , k . Value ) )
245+ . ToArray ( )
246+ ) ;
247+
248+ Info ( summary ) ;
249+ }
250+ catch ( Exception ex )
251+ {
252+ Error ( string . Format ( "Processing failed: {0}" , ex . Message ) ) ;
253+ }
0 commit comments