208 lines
5.5 KiB
PowerShell
208 lines
5.5 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Décompresse la section ```compressed-json``` d'un fichier .excalidraw.md (Obsidian/Excalidraw).
|
|
Utilise Node.js + npm pour accéder à la librairie LZ-String officielle.
|
|
|
|
.PARAMETER Path
|
|
Chemin du .excalidraw.md
|
|
|
|
.PARAMETER OutFile
|
|
Fichier JSON de sortie (optionnel). Par défaut: <Path>.decompressed.json
|
|
#>
|
|
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Path,
|
|
[string]$OutFile
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
Write-Host "❌ Fichier introuvable: $Path" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# --- Lecture et extraction du bloc compressed-json --------------------------
|
|
$content = Get-Content -LiteralPath $Path -Raw
|
|
|
|
$encoded = $null
|
|
if ($content -match '```compressed-json\s*([\s\S]*?)\s*```') {
|
|
$encoded = $matches[1]
|
|
} elseif ($content -match '%%\s*compressed-json\s*([\s\S]*?)\s*%%') {
|
|
$encoded = $matches[1]
|
|
} else {
|
|
Write-Host "⚠️ Aucune section compressed-json trouvée (ni fenced ``` ni %%)." -ForegroundColor Yellow
|
|
exit 2
|
|
}
|
|
|
|
$encoded = ($encoded -replace '[^A-Za-z0-9\+\/\=]', '').Trim()
|
|
if (-not $encoded) {
|
|
Write-Host "⚠️ Section compressed-json vide après nettoyage." -ForegroundColor Yellow
|
|
exit 3
|
|
}
|
|
|
|
if (-not $OutFile) {
|
|
$OutFile = [System.IO.Path]::ChangeExtension($Path, ".decompressed.json")
|
|
}
|
|
|
|
# --- Vérifier Node.js ---
|
|
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
if (-not $node) {
|
|
Write-Host "❌ Node.js n'est pas installé ou non trouvé dans PATH." -ForegroundColor Red
|
|
Write-Host " Installez Node.js depuis https://nodejs.org/" -ForegroundColor Yellow
|
|
exit 5
|
|
}
|
|
|
|
# --- Créer un fichier temporaire Node.js ---
|
|
$tempDir = [System.IO.Path]::GetTempPath()
|
|
$tempScript = Join-Path $tempDir "decompress_lz.js"
|
|
$tempOutput = Join-Path $tempDir "lz_output.json"
|
|
|
|
# Créer le script Node.js
|
|
$nodeScript = @"
|
|
// Décompression LZ-String manuelle (compatible avec compressToBase64)
|
|
const keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
|
|
function decompressFromBase64(input) {
|
|
if (!input) return "";
|
|
|
|
// Décodage base64 vers tableau d'octets
|
|
let bytes = [];
|
|
for (let i = 0; i < input.length; i += 4) {
|
|
let b1 = keyStrBase64.indexOf(input[i]) || 0;
|
|
let b2 = keyStrBase64.indexOf(input[i + 1]) || 0;
|
|
let b3 = keyStrBase64.indexOf(input[i + 2]) || 0;
|
|
let b4 = keyStrBase64.indexOf(input[i + 3]) || 0;
|
|
|
|
bytes.push((b1 << 2) | (b2 >> 4));
|
|
if (b3 < 64) bytes.push(((b2 & 15) << 4) | (b3 >> 2));
|
|
if (b4 < 64) bytes.push(((b3 & 3) << 6) | b4);
|
|
}
|
|
|
|
// Conversion en UTF16 (caractères)
|
|
let compressed = "";
|
|
for (let i = 0; i < bytes.length; i += 2) {
|
|
let c = bytes[i] | (bytes[i + 1] << 8 || 0);
|
|
compressed += String.fromCharCode(c);
|
|
}
|
|
|
|
return decompressFromUTF16(compressed);
|
|
}
|
|
|
|
function decompressFromUTF16(compressed) {
|
|
if (!compressed) return "";
|
|
|
|
let dict = [];
|
|
let dictSize = 4;
|
|
let numBits = 3;
|
|
let dataIdx = 0;
|
|
let bitPos = 0;
|
|
|
|
function readBits(n) {
|
|
let result = 0;
|
|
for (let i = 0; i < n; i++) {
|
|
if (bitPos >= compressed.length * 16) return result;
|
|
let charIdx = Math.floor(bitPos / 16);
|
|
let bitOffset = bitPos % 16;
|
|
let bit = (compressed.charCodeAt(charIdx) >> bitOffset) & 1;
|
|
result |= (bit << i);
|
|
bitPos++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Lire le premier code
|
|
let c = readBits(2);
|
|
if (c === 0) {
|
|
let val = readBits(8);
|
|
dict.push(String.fromCharCode(val));
|
|
} else if (c === 1) {
|
|
let val = readBits(16);
|
|
dict.push(String.fromCharCode(val));
|
|
} else if (c === 2) {
|
|
return "";
|
|
}
|
|
|
|
let w = dict[dict.length - 1];
|
|
let result = w;
|
|
|
|
while (true) {
|
|
c = readBits(numBits);
|
|
|
|
if (c === 0) {
|
|
let val = readBits(8);
|
|
dict.push(String.fromCharCode(val));
|
|
c = dict.length - 1;
|
|
} else if (c === 1) {
|
|
let val = readBits(16);
|
|
dict.push(String.fromCharCode(val));
|
|
c = dict.length - 1;
|
|
} else if (c === 2) {
|
|
return result;
|
|
}
|
|
|
|
let entry;
|
|
if (c < dict.length) {
|
|
entry = dict[c];
|
|
} else if (c === dict.length) {
|
|
entry = w + w[0];
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
result += entry;
|
|
|
|
if (dict.length < 65536) {
|
|
dict.push(w + entry[0]);
|
|
}
|
|
|
|
if (dict.length >= (1 << numBits)) {
|
|
numBits++;
|
|
}
|
|
|
|
w = entry;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const input = process.argv[2];
|
|
const output = decompressFromBase64(input);
|
|
const fs = require('fs');
|
|
fs.writeFileSync(process.argv[3], output, 'utf8');
|
|
console.log('OK');
|
|
} catch (err) {
|
|
console.error('ERROR: ' + err.message);
|
|
process.exit(1);
|
|
}
|
|
"@
|
|
|
|
$nodeScript | Out-File -FilePath $tempScript -Encoding utf8
|
|
|
|
try {
|
|
Write-Host "📦 Tentative de décompression..." -ForegroundColor Cyan
|
|
Write-Host " Base64 length: $($encoded.Length) caractères" -ForegroundColor Gray
|
|
|
|
# Exécuter le script Node.js en passant le base64 directement
|
|
$output = & node $tempScript $encoded $tempOutput 2>&1
|
|
|
|
if ($LASTEXITCODE -ne 0 -or $output -contains "ERROR") {
|
|
Write-Host "❌ Erreur de décompression: $output" -ForegroundColor Red
|
|
exit 4
|
|
}
|
|
|
|
if (Test-Path $tempOutput) {
|
|
Copy-Item $tempOutput $OutFile -Force
|
|
$json = Get-Content $tempOutput -Raw
|
|
Write-Host "✅ Décompression OK → $OutFile" -ForegroundColor Green
|
|
Write-Host " Taille JSON: $($json.Length) caractères" -ForegroundColor Gray
|
|
} else {
|
|
Write-Host "❌ Le fichier de sortie n'a pas été créé." -ForegroundColor Red
|
|
exit 4
|
|
}
|
|
} catch {
|
|
Write-Host "❌ Erreur lors de l'exécution: $_" -ForegroundColor Red
|
|
exit 4
|
|
} finally {
|
|
# Nettoyage
|
|
Remove-Item $tempScript -ErrorAction SilentlyContinue
|
|
Remove-Item $tempOutput -ErrorAction SilentlyContinue
|
|
} |