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+ StringComparison . OrdinalIgnoreCase
116+ ) ;
117+ } ;
118+
119+ Func < JToken , bool > IsEmpty = delegate ( JToken t ) {
120+ return t == null || t . Type == JTokenType . Null ||
121+ ( t is JContainer && ! ( ( JContainer ) t ) . HasValues ) ||
122+ ( t . Type == JTokenType . String && string . IsNullOrWhiteSpace ( ( string ) t ) ) ;
123+ } ;
124+
125+ // Recursive cleaner
126+ Action < JToken > Clean = null ;
127+ Clean = delegate ( JToken token ) {
128+ if ( token == null ) return ;
129+
130+ if ( token . Type == JTokenType . Object ) {
131+ var obj = ( JObject ) token ;
132+
133+ // Recurse first (depth-first)
134+ foreach ( var prop in obj . Properties ( ) . ToList ( ) ) Clean ( prop . Value ) ;
135+
136+ var toRemove = new List < JProperty > ( ) ;
137+
138+ foreach ( var prop in obj . Properties ( ) ) {
139+ // Name-based removals
140+ if ( dropKeys . Contains ( prop . Name ) ) {
141+ toRemove . Add ( prop ) ;
142+ Track ( prop . Name ) ;
143+ continue ;
144+ }
145+
146+ // formatString special handling (protect formatStringDefinition)
147+ if ( REMOVE_FormatString && Eq ( prop . Name , "formatString" ) ) {
148+ toRemove . Add ( prop ) ;
149+ Track ( "formatString" ) ;
150+ continue ;
151+ }
152+
153+ // Empty container removal (with structural preservation)
154+ if ( REMOVE_EmptyContainers && IsEmpty ( prop . Value ) && ! preserve . Contains ( prop . Name ) ) {
155+ toRemove . Add ( prop ) ;
156+ Track ( "empty" ) ;
157+ continue ;
158+ }
159+ }
160+
161+ // Value-based removals (checked after structure scan)
162+ if ( REMOVE_DefaultValues ) {
163+ var dc = obj . Property ( "dataCategory" , StringComparison . OrdinalIgnoreCase ) ;
164+ if ( dc != null && dc . Value is JValue && Eq ( ( string ) dc . Value , "Uncategorized" ) ) {
165+ toRemove . Add ( dc ) ;
166+ Track ( "dataCategory=default" ) ;
167+ }
168+
169+ var sb = obj . Property ( "summarizeBy" , StringComparison . OrdinalIgnoreCase ) ;
170+ if ( sb != null && sb . Value is JValue && Eq ( ( string ) sb . Value , "none" ) ) {
171+ toRemove . Add ( sb ) ;
172+ Track ( "summarizeBy=none" ) ;
173+ }
174+ }
175+
176+ if ( REMOVE_RedundantNames ) {
177+ var name = obj . Property ( "name" , StringComparison . OrdinalIgnoreCase ) ;
178+ if ( name != null && name . Value is JValue ) {
179+ var nameStr = ( string ) name . Value ;
180+ var nameBracketed = nameStr != null ? string . Format ( "[{0}]" , nameStr ) : null ;
181+
182+ var src = obj . Property ( "sourceColumn" , StringComparison . OrdinalIgnoreCase ) ;
183+ if (
184+ src != null &&
185+ src . Value is JValue &&
186+ (
187+ Eq ( ( string ) src . Value , nameStr ) ||
188+ Eq ( ( string ) src . Value , nameBracketed )
189+ )
190+ ) {
191+ toRemove . Add ( src ) ;
192+ Track ( "sourceColumn=name" ) ;
193+ }
194+
195+ var disp = obj . Property ( "displayName" , StringComparison . OrdinalIgnoreCase ) ;
196+ if (
197+ disp != null &&
198+ disp . Value is JValue &&
199+ (
200+ Eq ( ( string ) disp . Value , nameStr ) ||
201+ Eq ( ( string ) disp . Value , nameBracketed )
202+ )
203+ ) {
204+ toRemove . Add ( disp ) ;
205+ Track ( "displayName=name" ) ;
206+ }
207+ }
208+ }
209+
210+ // Apply all removals
211+ foreach ( var prop in toRemove . Distinct ( ) ) prop . Remove ( ) ;
212+ }
213+ else if ( token . Type == JTokenType . Array ) {
214+ var arr = ( JArray ) token ;
215+ foreach ( var item in arr . ToList ( ) ) {
216+ Clean ( item ) ;
217+ if ( REMOVE_EmptyContainers && IsEmpty ( item ) ) {
218+ item . Remove ( ) ;
219+ Track ( "empty" ) ;
220+ }
221+ }
222+ }
223+ } ;
224+
225+ // Execute cleaning
226+ Clean ( root ) ;
227+
228+ // Save result
229+ var formatting = MINIFY_OUTPUT ? Formatting . None : Formatting . Indented ;
230+ File . WriteAllText ( outputPath , root . ToString ( formatting ) ) ;
231+
232+ // Report results
233+ var newSize = new FileInfo ( outputPath ) . Length ;
234+ var reduction = ( 1.0 - ( double ) newSize / originalSize ) * 100 ;
235+ var summary =
236+ "BIM Slimmer Results\n " +
237+ "==================\n " +
238+ string . Format ( "Input: {0} ({1:N1} KB)\n " , Path . GetFileName ( inputPath ) , originalSize / 1024.0 ) +
239+ string . Format ( "Output: {0} ({1:N1} KB)\n " , Path . GetFileName ( outputPath ) , newSize / 1024.0 ) +
240+ string . Format ( "Saved: {0:F1}%\n \n " , reduction ) +
241+ string . Format ( "Removed: {0:N0} items\n " , stats . Values . Sum ( ) ) +
242+ string . Join (
243+ "\n " ,
244+ stats . OrderBy ( k => k . Key )
245+ . Select ( k => string . Format ( " • {0}: {1:N0}" , k . Key , k . Value ) )
246+ . ToArray ( )
247+ ) ;
248+
249+ Info ( summary ) ;
250+ }
251+ catch ( Exception ex )
252+ {
253+ Error ( string . Format ( "Processing failed: {0}" , ex . Message ) ) ;
254+ }
0 commit comments