Correcteur de fichiers PBIX (Dashboard Patcher)
Modifier un fichier .pbix de Power BI Desktop sur place : ajouter des pages, ajouter des visuels, modifier la disposition, rebinder des champs — puis le reconditionner pour que Power BI Desktop l'ouvre sans erreur de corruption.
Quand l'utiliser
- L'utilisateur a un fichier
.pbixenregistré et souhaite ajouter des visuels ou générer un tableau de bord. - L'utilisateur souhaite automatiser les modifications de rapports Power BI (CI, modélisation en masse, disposition basée sur les données).
- Une tentative de modification précédente a produit
MashupValidationError,Invalid SecurityBindingsou un message de fichier corrompu à l'ouverture.
Le problème central (à lire en premier)
Les fichiers .pbix modernes utilisent le format PBIR (Enhanced Report Format) : la disposition du rapport est stockée sous forme de fichiers JSON par visuel dans Report/definition/pages/<pageId>/visuals/<visualId>/visual.json, et non plus dans l'ancien fichier monolithique Report/Layout.
Un fichier PBIX est une archive ZIP contenant (entre autres) :
DataModel— le modèle Analysis Services (ne doit PAS être recompressé ; à conserver tel quel).Report/definition/...— l'arborescence JSON PBIR par visuel.SecurityBindings— un manifeste chiffré via DPAPI qui signe la disposition du fichier.[Content_Types].xml— liste chaque fichier présent dans l'archive ZIP.
Pièges qui corrompent le fichier (tirés de l'expérience) :
zipfilede Python définit des attributs de fichier externes différents de ceux deZipArchivede .NET. Power BI les valide. UtilisezZipArchivede .NET via PowerShell, avecExternalAttributes = 0sur chaque entrée.- Recompresser
DataModelle corrompt. Il doit être stocké avec la même méthode de compression que la source (généralementDeflatemais parfoisStored) ; le plus sûr estNoCompressionsi vous l'avez réécrit, sinon conservez les octets originaux tels quels. - L'ajout d'un fichier casse
SecurityBindings. C'est un manifeste signé de ce qui se trouve dans l'archive ZIP. Si vous ajoutezReport/definition/pages/<new>/visuals/.../visual.json, la signature ne correspond plus et Power BI refuse d'ouvrir le fichier. - Les noms d'entrée ne doivent PAS avoir de
/au début.MissingBIPackagePart: /Versionà l'ouverture signifie que votre entréeVersiona été compressée sous la forme/Version(slash de tête).Substring($src.Length + 1)naïf de PowerShell laisse une barre oblique inverse au début → devient/dans l'archive ZIP → Power BI rejette. Calculez le préfixe à partir du chemin long résolu et appliquezTrimStart('/')à chaque nom d'entrée. - La marque BOM UTF-8 dans
Report/definition/**/*.jsonest fatale. Erreur de Power BI :Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'.Set-Content -Encoding UTF8etFile.WriteAllTextsans encodage explicite écrivent TOUS DEUX une marque BOM dans Windows PowerShell 5.1 / .NET Framework. Écrivez toujours le JSON avecNew-Object System.Text.UTF8Encoding($false)passé àWriteAllTextouStreamWriter.
La solution pour SecurityBindings : supprimez-le de l'archive ZIP et retirez son entrée de [Content_Types].xml. Power BI Desktop régénère SecurityBindings lors de la prochaine sauvegarde. Le fichier s'ouvre proprement.
Procédure
1. Inspecter le fichier
# Extraire vers un répertoire de travail en utilisant .NET pour que les attributs soient propres
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")
Vérifiez s'il s'agit du format PBIR (nouveau) ou hérité :
- PBIR : contient
Report\definition\pages\<pageId>\visuals\<visualId>\visual.json. - Hérité : contient uniquement
Report\Layout(chaîne JSON binaire). Cette compétence cible le format PBIR.
2. Comprendre le schéma visual.json (version 2.12.0)
Visuel cardVisual minimal (tuile KPI) :
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
"name": "<id hexadécimale à 20 caractères>",
"position": {"x": 20, "y": 20, "z": 0, "height": 160, "width": 440, "tabOrder": 0},
"visual": {
"visualType": "cardVisual",
"query": {
"queryState": {
"Data": {
"projections": [{
"field": {
"Aggregation": {
"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}},
"Function": 0
}
},
"queryRef": "Sum(financials.Sales)",
"nativeQueryRef": "Sum of Sales",
"active": true
}]
}
},
"sortDefinition": {
"sort": [{"field": {"Aggregation": {"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}}, "Function": 0}}, "direction": "Descending"}],
"isDefaultSort": true
}
},
"drillFilterOtherVisuals": true
}
}
Points clés :
nameest un identifiant hexadécimal de 20 caractères ; doit être unique dans la page.- Exemples de
visualType:cardVisual,clusteredBarChart,clusteredColumnChart,lineChart,donutChart,pieChart,tableEx,pivotTable. cardVisualutilise le rôleData. Les graphiques à barres/colonnes/lignes utilisentCategory+Y(+Seriesoptionnel). Les graphiques en anneau/tarte utilisentCategory+Y.Aggregation.Function: 0=Sum, 1=Avg, 2=Count, 3=Min, 4=Max, 5=CountNonNull, 6=Median, 7=StDev, 8=Variance.drillFilterOtherVisuals: trueest obligatoire ; son omission corrompt le fichier.
3. Enregistrer la page
Report/definition/pages/pages.json :
{"$schema":"...pagesMetadata/1.1.0/schema.json","pageOrder":["<pageId1>","<pageId2>"],"activePageName":"<pageIdX>"}
Chaque dossier de page contient un fichier page.json :
{"$schema":"...page/2.1.0/schema.json","name":"<pageId>","displayName":"Dashboard","displayOption":"FitToPage","height":1080,"width":1920}
4. Écrire les fichiers JSON — UTF-8 SANS BOM
Chaque fois que vous modifiez visual.json, page.json, report.json ou tout fichier sous Report/definition/, écrivez-le sans BOM. Windows PowerShell 5.1 ajoute une BOM par défaut.
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $json, $utf8NoBom)
Pour supprimer une BOM déjà écrite par erreur :
$b = [System.IO.File]::ReadAllBytes($path)
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
$text = [System.Text.Encoding]::UTF8.GetString($b, 3, $b.Length - 3)
[System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false)))
}
5. Reconditionner — la SEULE méthode fiable
N'utilisez PAS zipfile de Python. Utilisez PowerShell avec ZipArchive de .NET. Détails clés :
- Les noms d'entrée utilisent des barres obliques et n'ont PAS de
/au début. - Calculez la longueur du préfixe source à partir de
(Get-Item $src).FullName.TrimEnd('\').Length + 1(même casse/forme de chemin long queFullNamedes éléments Get-ChildItem). AppliquezTrimStart('/')à chaque nom d'entrée par sécurité. - Chaque entrée de texte (y compris
[Content_Types].xml) est écrite avec unStreamWriterUTF-8 sans BOM.
Add-Type -AssemblyName System.IO.Compression
$src = "pbix_src"
$dst = "Test_patched.pbix"
if (Test-Path $dst) { Remove-Item $dst }
$srcFull = (Get-Item $src).FullName.TrimEnd('\')
$prefixLen = $srcFull.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$fs = [System.IO.File]::Open($dst, 'Create')
$zip = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem $src -Recurse -File | ForEach-Object {
$full = $_.FullName
$rel = $full.Substring($prefixLen).Replace('\','/').TrimStart('/')
# SUPPRIMER SecurityBindings — PBI le régénère lors de la sauvegarde
if ($rel -eq 'SecurityBindings') { return }
# Modifier Content_Types.xml pour supprimer la référence à SecurityBindings
if ($rel -eq '[Content_Types].xml') {
$xml = [System.IO.File]::ReadAllText($full) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
$entry = $zip.CreateEntry($rel, [System.IO.Compression.CompressionLevel]::Optimal)
$entry.ExternalAttributes = 0
$sw = New-Object System.IO.StreamWriter($entry.Open(), $utf8NoBom)
$sw.Write($xml); $sw.Close()
return
}
# DataModel ne doit PAS être recompressé
$level = if ($rel -eq 'DataModel') {
[System.IO.Compression.CompressionLevel]::NoCompression
} else {
[System.IO.Compression.CompressionLevel]::Optimal
}
$entry = $zip.CreateEntry($rel, $level)
$entry.ExternalAttributes = 0 # critique — Python définit une valeur non nulle que PBI rejette
$es = $entry.Open()
$sfs = [System.IO.File]::OpenRead($full)
$sfs.CopyTo($es)
$sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()
6. Valider avant d'ouvrir
Quatre vérifications, dans l'ordre :
Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead("Test_patched.pbix")
# a) Parties requises présentes, SecurityBindings absent
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
Select-Object FullName, Length
# b) Aucune entrée avec un '/' au début
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "MAUVAIS : $($_.FullName)" }
# c) Aucune BOM sur les métadonnées textuelles ou les JSON Report/definition
foreach ($e in $z.Entries) {
if ($e.FullName -match '\.(json|xml)$|^Version$|^Metadata$|^Settings$') {
$s = $e.Open(); $ms = New-Object System.IO.MemoryStream; $s.CopyTo($ms); $s.Close()
$b = $ms.ToArray()
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
Write-Host "BOM trouvée : $($e.FullName)"
}
}
}
# d) Chaque JSON Report/definition s'analyse
foreach ($e in $z.Entries) {
if ($e.FullName -like 'Report/definition/*.json' -or $e.FullName -like 'Report/definition/**/*.json') {
$s = $e.Open(); $sr = New-Object System.IO.StreamReader($s)
$txt = $sr.ReadToEnd(); $sr.Close()
try { $null = $txt | ConvertFrom-Json } catch { "JSON INVALIDE : $($e.FullName) — $($_.Exception.Message)" }
}
}
$z.Dispose()
Attendu : DataModel, Version, Metadata, Settings, [Content_Types].xml présents ; SecurityBindings absent ; pas de barre oblique de tête ; pas de BOM ; tous les JSON s'analysent.
Connexion à une session Power BI Desktop en direct (optionnel — pour la découverte des données)
Si Power BI Desktop est déjà en cours d'exécution, il expose un point de terminaison Analysis Services sur localhost :
# Découvrir le port
Get-Process msmdsrv | ForEach-Object {
$port = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
Select-Object -First 1 -ExpandProperty LocalPort
Write-Host "Port Analysis Services : $port"
}
Connectez-vous ensuite via ADOMD (installez Power BI Desktop ; la DLL est fournie avec) :
# pip install pyadomd pythonnet
import clr
clr.AddReference(r"C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll")
from Microsoft.AnalysisServices.AdomdClient import AdomdConnection # espace de noms réel, pas le nom du fichier
conn = AdomdConnection(f"Data Source=localhost:{port}")
conn.Open()
# Exécuter DAX : EVALUATE SUMMARIZECOLUMNS(financials[Segment], "Sales", SUM(financials[Sales]))
Utilisez ceci uniquement pour lire le schéma et prévisualiser les nombres ; les visuels finaux sont écrits sous forme de fichiers JSON et reconditionnés comme ci-dessus.
Styles des visuels — les noms de propriétés qui fonctionnent réellement
Les libellés du volet de mise en forme dans Power BI Desktop ne correspondent PAS aux noms de propriétés JSON. Si vous les devinez, Power BI supprime silencieusement les propriétés invalides lors de la prochaine sauvegarde — le fichier s'ouvre correctement, mais votre style disparaît. Construisez toujours les modifications réelles dans Power BI une fois, enregistrez, extrayez, et copiez les noms de propriétés à partir du JSON enregistré.
Où se trouve le style dans visual.json
visual.visualContainerObjects— le cadre extérieur de la vignette du visuel (arrière-plan, bordure, ombre portée, barre de titre). S'applique à tous les types de visuels.visual.objects— l'intérieur du visuel (zone de traçage, axes, légende, points de données, valeur de la carte, remplissage intérieur de la carte). Les noms de propriétés ici sont spécifiques à chaque type de visuel et doivent correspondre au schémavisualStyles[visualType]du thème.
Cadre du conteneur (fonctionne pour chaque visuel)
"visualContainerObjects": {
"background": [{"properties":{
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#EEF2FF'"}}}}},
"transparency":{"expr":{"Literal":{"Value":"0D"}}}
}}],
"border": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E2E8F0'"}}}}},
"radius":{"expr":{"Literal":{"Value":"12D"}}}
}}],
"dropShadow": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#0F172A'"}}}}},
"transparency":{"expr":{"Literal":{"Value":"85D"}}},
"position":{"expr":{"Literal":{"Value":"'Outer'"}}},
"preset":{"expr":{"Literal":{"Value":"'BottomRight'"}}}
}}]
}
cardVisual (tuile KPI) — pièges
Le grand nombre n'est PAS callout.color et PAS value.color. Power BI supprime silencieusement les deux.
- Couleur de la police du grand nombre :
value.fontColor(pascolor). - Police / taille / graisse du grand nombre :
value.fontFamily,value.fontSize,value.bold. - Étiquette de catégorie sous le nombre :
label.fontColor,label.fontFamily,label.fontSize. - Remplissage teinté derrière le nombre :
cardCalloutArea.backgroundFillColor, avec unselectorqui nomme les métadonnées de la projection de mesure — par exemple"selector":{"metadata":"Sum(financials.Profit)"}. Lisez cette chaîne à partir devisual.query.queryState.Data.projections[0].queryRef.
"objects": {
"value": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#4F46E5'"}}}}},
"fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
"fontSize":{"expr":{"Literal":{"Value":"40D"}}},
"bold":{"expr":{"Literal":{"Value":"true"}}}
}}],
"label": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#64748B'"}}}}}
}}],
"cardCalloutArea": [{
"properties": {
"show":{"expr":{"Literal":{"Value":"true"}}},
"backgroundFillColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E0E7FF'"}}}}}
},
"selector":{"metadata":"Sum(financials.Sales)"}
}]
}
Graphiques à barres / colonnes / lignes / anneau — couleurs des données
dataPoint.defaultColor— la couleur de repli utilisée lorsqu'aucune couleur n'est attribuée à des catégories spécifiques.dataPoint.fill— parfois utilisé à la place dedefaultColor, en fonction du type de visuel.
"objects": {
"dataPoint": [{"properties":{
"defaultColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#3B82F6'"}}}}}
}}]
}
Tableau et matrice
grid: propriétés de la grille (bordures, taille des lignes).columnHeaders: police, alignement, couleur de fond de l'en-tête.rowHeaders: police, alignement, couleur de fond de la première colonne.values: police des cellules de valeur.
"objects": {
"grid": [{"properties":{
"outlineColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#CBD5E1'"}}}}},
"horizontalGridLineStyle":{"expr":{"Literal":{"Value":"'Dotted'"}}}
}}],
"columnHeaders": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#1E293B'"}}}}},
"backgroundColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#F1F5F9'"}}}}},
"fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
"fontSize":{"expr":{"Literal":{"Value":"12D"}}}
}}],
"values": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#334155'"}}}}}
}}]
}
Filtres au niveau du rapport (dans report.json)
{
"$schema": "...report/2.5.0/schema.json",
"filters": [{
"filter": {
"$schema": "...filterConfiguration/2.0.0/schema.json",
"expression": {
"And": {
"left": {
"Comparison": {
"comparisonType": 2,
"left": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Year"}},
"right": {"Literal": {"Value": "'2024'"}}
}
}
}
},
"filterState": {"version": 2, "members": [{"memberName": "(All)"}]},
"filterScope": {"is": {"All": {}}}
}
}]
}
Types de comparaison : 0=Égal, 1=Différent, 2=Supérieur, 3=Supérieur ou égal, 4=Inférieur, 5=Inférieur ou égal.
Résumé — Script de correction complet
# 1. Extraire
$srcPbix = "Original.pbix"
$outDir = "pbix_wip"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($srcPbix, $outDir)
# 2. Ajouter ou modifier des pages/visuels (détaillé ci-dessus)
# ... vos modifications JSON ici ...
# 3. Supprimer SecurityBindings et sa référence dans Content_Types.xml
Remove-Item "$outDir\SecurityBindings" -ErrorAction SilentlyContinue
$ctPath = "$outDir\[Content_Types].xml"
$xml = [System.IO.File]::ReadAllText($ctPath) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
[System.IO.File]::WriteAllText($ctPath, $xml, (New-Object System.Text.UTF8Encoding($false)))
# 4. Reconditionner (codes ci-dessus)
# ...
Write-Host "Terminé. Ouvrez Test_patched.pbix dans Power BI Desktop."