param( [Parameter(Mandatory=$false)][string]$Ruta, [Parameter(Mandatory=$false)][string]$Salida ) $ErrorActionPreference = 'SilentlyContinue' Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName Microsoft.VisualBasic try { Add-Type -AssemblyName System.Drawing } catch {} try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch {} if ([string]::IsNullOrWhiteSpace($Ruta)) { $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = 'Seleccione la carpeta documental que desea enriquecer' $dialog.ShowNewFolderButton = $false if ($dialog.ShowDialog() -ne 'OK') { exit } $Ruta = $dialog.SelectedPath } $Ruta = (Resolve-Path $Ruta).Path if ([string]::IsNullOrWhiteSpace($Salida)) { $Salida = Join-Path $Ruta ('metadatos_windows_avanzados_{0}.json' -f (Get-Date -Format 'yyyyMMdd_HHmmss')) } Write-Host 'Gestion Documental SENA - Motor Windows de enriquecimiento v23.0' -ForegroundColor Green Write-Host 'Extraccion complementaria SOLO LECTURA. No modifica ni elimina documentos.' -ForegroundColor Cyan Write-Host "Ruta: $Ruta" $shell = New-Object -ComObject Shell.Application $word = $null $excel = $null $powerpoint = $null $exifToolCmd = Get-Command exiftool.exe -ErrorAction SilentlyContinue if (-not $exifToolCmd) { $exifToolCmd = Get-Command exiftool -ErrorAction SilentlyContinue } $droidCmd = Get-Command droid.bat -ErrorAction SilentlyContinue if (-not $droidCmd) { $droidCmd = Get-Command droid -ErrorAction SilentlyContinue } if ($exifToolCmd) { Write-Host ('ExifTool detectado: ' + $exifToolCmd.Source) -ForegroundColor Cyan } else { Write-Host 'ExifTool no detectado. La extraccion continuara con Windows/Office; puede instalar ExifTool para enriquecer aun mas los metadatos.' -ForegroundColor Yellow } if ($droidCmd) { Write-Host ('DROID detectado: ' + $droidCmd.Source + ' (se registra disponibilidad; la identificacion PRONOM se recomienda como diagnostico complementario).') -ForegroundColor Cyan } function Get-ShellProperties([string]$FilePath) { $item = Get-Item -LiteralPath $FilePath $folder = $shell.Namespace($item.Directory.FullName) $shellItem = $folder.ParseName($item.Name) $props = [ordered]@{} for ($i=0; $i -lt 420; $i++) { $name = $folder.GetDetailsOf($folder.Items, $i) if ($name -and -not $props.Contains($name)) { $value = $folder.GetDetailsOf($shellItem, $i) if ($value) { $props[$name] = $value } } } return $props } function Find-Prop($props, [string[]]$names) { foreach ($name in $names) { foreach ($key in $props.Keys) { if ([string]::Equals([string]$key,[string]$name,[System.StringComparison]::OrdinalIgnoreCase)) { return $props[$key] } } } return '' } function Get-BuiltinProperty($collection, [string]$name) { try { return $collection.Item($name).Value } catch { return '' } } function Get-NativeOfficeData([System.IO.FileInfo]$file, $props) { $ext = $file.Extension.ToLowerInvariant() $out = [ordered]@{ pages=''; pageMethod=''; officeProperties=[ordered]@{}; sheets=''; slides=''; words=''; characters=''; paragraphs='' } $shellPages = Find-Prop $props @('Número de páginas','Number of pages','Páginas','Pages') if ($shellPages) { $out.pages = $shellPages; $out.pageMethod = 'Propiedad Windows Shell' } if ($ext -in @('.doc','.docx','.docm','.dot','.dotx','.rtf','.odt')) { try { if ($null -eq $script:word) { $script:word = New-Object -ComObject Word.Application $script:word.Visible = $false $script:word.DisplayAlerts = 0 } $doc = $script:word.Documents.Open($file.FullName, $false, $true) $out.pages = $doc.ComputeStatistics(2) $out.words = $doc.ComputeStatistics(0) $out.characters = $doc.ComputeStatistics(3) $out.paragraphs = $doc.ComputeStatistics(4) $out.pageMethod = 'Microsoft Word ComputeStatistics' $bp = $doc.BuiltInDocumentProperties foreach ($n in @('Title','Subject','Author','Keywords','Comments','Last author','Revision number','Creation date','Last save time','Last print date','Template','Company','Manager')) { $v = Get-BuiltinProperty $bp $n; if ($v) { $out.officeProperties[$n] = [string]$v } } $doc.Close($false) return $out } catch {} } if ($ext -in @('.ppt','.pptx','.pptm','.odp')) { try { if ($null -eq $script:powerpoint) { $script:powerpoint = New-Object -ComObject PowerPoint.Application } $presentation = $script:powerpoint.Presentations.Open($file.FullName, $true, $false, $false) $out.pages = $presentation.Slides.Count $out.slides = $presentation.Slides.Count $out.pageMethod = 'Microsoft PowerPoint - diapositivas' try { $bp = $presentation.BuiltInDocumentProperties foreach ($n in @('Title','Subject','Author','Keywords','Comments','Last author','Revision number','Creation date','Last save time','Last print date','Company','Manager')) { $v = Get-BuiltinProperty $bp $n; if ($v) { $out.officeProperties[$n] = [string]$v } } } catch {} $presentation.Close() return $out } catch {} } if ($ext -in @('.xls','.xlsx','.xlsm','.xlsb','.ods')) { try { if ($null -eq $script:excel) { $script:excel = New-Object -ComObject Excel.Application $script:excel.Visible = $false $script:excel.DisplayAlerts = $false } $book = $script:excel.Workbooks.Open($file.FullName, 0, $true) $pages = 0; $sheetNames = New-Object System.Collections.Generic.List[string] foreach ($sheet in $book.Worksheets) { [void]$sheetNames.Add([string]$sheet.Name) try { $sheet.Activate() | Out-Null; $count = $sheet.PageSetup.Pages.Count; if ($count -lt 1) { $count = 1 }; $pages += $count } catch {} } $out.pages = $pages $out.sheets = $book.Worksheets.Count $out.officeProperties['Nombres de hojas'] = ($sheetNames -join ' | ') $out.pageMethod = 'Microsoft Excel - paginas de impresion' try { $bp = $book.BuiltinDocumentProperties foreach ($n in @('Title','Subject','Author','Keywords','Comments','Last author','Revision number','Creation date','Last save time','Last print date','Company','Manager')) { $v = Get-BuiltinProperty $bp $n; if ($v) { $out.officeProperties[$n] = [string]$v } } } catch {} $book.Close($false) return $out } catch {} } return $out } function Get-ExifToolData([System.IO.FileInfo]$file) { $o = [ordered]@{} if (-not $script:exifToolCmd) { return $o } try { $raw = & $script:exifToolCmd.Source '-j' '-G1' '-a' '-s' '-charset' 'filename=UTF8' $file.FullName 2>$null | Out-String if ([string]::IsNullOrWhiteSpace($raw)) { return $o } $parsed = $raw | ConvertFrom-Json $item = if ($parsed -is [System.Array]) { $parsed[0] } else { $parsed } if ($item) { foreach ($prop in $item.PSObject.Properties) { if ($prop.Name -eq 'SourceFile') { continue } $v = $prop.Value if ($null -eq $v) { continue } if ($v -is [System.Array]) { $v = ($v -join ' | ') } elseif ($v -isnot [string] -and $v -isnot [ValueType]) { $v = ($v | ConvertTo-Json -Compress -Depth 4) } if (-not [string]::IsNullOrWhiteSpace([string]$v)) { $o[$prop.Name] = [string]$v } } } } catch {} return $o } function Get-PdfData([System.IO.FileInfo]$file) { $o = [ordered]@{} if ($file.Extension.ToLowerInvariant() -ne '.pdf') { return $o } try { $bytes = [System.IO.File]::ReadAllBytes($file.FullName) $ascii = [System.Text.Encoding]::ASCII.GetString($bytes) $o['Version PDF'] = if ($ascii -match '%PDF-([0-9.]+)') { $Matches[1] } else { '' } $o['Paginas estructura local'] = ([regex]::Matches($ascii,'/Type\s*/Page\b')).Count $o['Cifrado'] = if ($ascii -match '/Encrypt\b') { 'Si' } else { 'No' } $o['Firma PDF'] = if ($ascii -match '/ByteRange\s*\[') { 'Estructura detectada' } else { 'No detectada' } $o['Sello de tiempo'] = if ($ascii -match 'ETSI\.RFC3161|DocTimeStamp') { 'Indicador detectado' } else { 'No detectado' } $o['Formulario AcroForm'] = if ($ascii -match '/AcroForm\b') { 'Si' } else { 'No' } } catch {} return $o } function Get-ImageData([System.IO.FileInfo]$file) { $o = [ordered]@{} if ($file.Extension.ToLowerInvariant() -notin @('.jpg','.jpeg','.png','.bmp','.gif','.tif','.tiff')) { return $o } try { $img = [System.Drawing.Image]::FromFile($file.FullName) $o['Ancho pixeles'] = $img.Width $o['Alto pixeles'] = $img.Height $o['Resolucion X'] = $img.HorizontalResolution $o['Resolucion Y'] = $img.VerticalResolution $o['PixelFormat'] = [string]$img.PixelFormat foreach ($id in $img.PropertyIdList) { try { $pi = $img.GetPropertyItem($id) if ($id -in @(271,272,306,36867,36868)) { $val = [System.Text.Encoding]::ASCII.GetString($pi.Value).Trim([char]0) switch ($id) { 271 {$o['Camara fabricante']=$val};272 {$o['Camara modelo']=$val};306 {$o['Fecha imagen']=$val};36867 {$o['Fecha captura original']=$val};36868 {$o['Fecha digitalizacion']=$val} } } } catch {} } $img.Dispose() } catch {} return $o } function Get-ZipData([System.IO.FileInfo]$file) { $o = [ordered]@{} if ($file.Extension.ToLowerInvariant() -notin @('.zip','.epub','.docx','.xlsx','.pptx','.docm','.xlsm','.pptm','.odt','.ods','.odp')) { return $o } try { $archive = [System.IO.Compression.ZipFile]::OpenRead($file.FullName) $o['Entradas ZIP'] = $archive.Entries.Count $o['Tamano descomprimido'] = ($archive.Entries | Measure-Object Length -Sum).Sum $archive.Dispose() } catch {} return $o } function Get-TextData([System.IO.FileInfo]$file) { $o = [ordered]@{} $ext = $file.Extension.ToLowerInvariant() if ($ext -notin @('.txt','.csv','.tsv','.log','.md','.json','.xml','.html','.htm','.css','.js','.ts','.py','.java','.c','.cpp','.cs','.php','.sql','.yaml','.yml','.ini','.cfg','.conf','.eml','.ics','.vcf','.rtf')) { return $o } try { $max = 5MB $fs = [System.IO.File]::OpenRead($file.FullName) $len = [math]::Min($fs.Length,$max) $buf = New-Object byte[] $len [void]$fs.Read($buf,0,$len); $fs.Close() $txt = [System.Text.Encoding]::UTF8.GetString($buf) $o['Lineas'] = ([regex]::Matches($txt,"`n")).Count + 1 $o['Palabras aproximadas'] = ([regex]::Matches($txt,'\S+')).Count $o['Caracteres muestra'] = $txt.Length $o['Lectura parcial'] = if ($file.Length -gt $max) { 'Si, primeros 5 MB' } else { 'No' } if ($ext -eq '.eml') { if ($txt -match '(?im)^Subject:\s*(.+)$') { $o['Asunto correo']=$Matches[1].Trim() } if ($txt -match '(?im)^From:\s*(.+)$') { $o['De']=$Matches[1].Trim() } if ($txt -match '(?im)^To:\s*(.+)$') { $o['Para']=$Matches[1].Trim() } $o['Adjuntos declarados'] = ([regex]::Matches($txt,'(?i)Content-Disposition:\s*attachment')).Count } if ($ext -eq '.ics') { $o['Eventos VEVENT'] = ([regex]::Matches($txt,'(?i)BEGIN:VEVENT')).Count } } catch {} return $o } $files = Get-ChildItem -LiteralPath $Ruta -Recurse -File -Force $result = New-Object System.Collections.Generic.List[object] $total = $files.Count for ($i=0; $i -lt $total; $i++) { $f = $files[$i] $pct = if ($total) { [math]::Round((($i+1)/$total)*100) } else { 100 } Write-Progress -Activity 'Extrayendo metadatos Windows' -Status "$($i+1) de $total - $($f.Name)" -PercentComplete $pct $props = Get-ShellProperties $f.FullName $office = Get-NativeOfficeData $f $props $relative = $f.FullName.Substring($Ruta.Length).TrimStart('\') $authors = Find-Prop $props @('Autores','Authors','Autor','Author') $title = Find-Prop $props @('Titulo','Título','Title') $subject = Find-Prop $props @('Asunto','Subject') $keywords = Find-Prop $props @('Etiquetas','Tags','Palabras clave','Keywords') $contentCreated = Find-Prop $props @('Contenido creado','Content created','Fecha de creación','Date created') $lastPrinted = Find-Prop $props @('Fecha de última impresión','Date last printed','Última impresión') $duration = Find-Prop $props @('Duración','Length','Duration') $dimensions = Find-Prop $props @('Dimensiones','Dimensions') $width = Find-Prop $props @('Ancho de fotograma','Frame width','Ancho','Width') $height = Find-Prop $props @('Alto de fotograma','Frame height','Alto','Height') $bitrate = Find-Prop $props @('Velocidad de bits','Bit rate','Total bitrate') $dateTaken = Find-Prop $props @('Fecha de captura','Date taken') $camera = Find-Prop $props @('Modelo de cámara','Camera model') if ($office.officeProperties['Author']) { $authors = $office.officeProperties['Author'] } if ($office.officeProperties['Title']) { $title = $office.officeProperties['Title'] } if ($office.officeProperties['Subject']) { $subject = $office.officeProperties['Subject'] } if ($office.officeProperties['Keywords']) { $keywords = $office.officeProperties['Keywords'] } if ($office.officeProperties['Creation date']) { $contentCreated = $office.officeProperties['Creation date'] } if ($office.officeProperties['Last print date']) { $lastPrinted = $office.officeProperties['Last print date'] } $specific = [ordered]@{} $exifData = Get-ExifToolData $f if ($exifData.Count -gt 0) { foreach ($key in $exifData.Keys) { $val = [string]$exifData[$key] if (-not $authors -and $key -match '(?i)(Author|Creator|Artist)$') { $authors = $val } if (-not $title -and $key -match '(?i)(Title|ObjectName)$') { $title = $val } if (-not $subject -and $key -match '(?i)(Subject|Description|Caption-Abstract)$') { $subject = $val } if (-not $keywords -and $key -match '(?i)(Keywords|Subject)$') { $keywords = $val } if (-not $contentCreated -and $key -match '(?i)(CreateDate|DateTimeOriginal|DocumentCreateDate)$') { $contentCreated = $val } if (-not $duration -and $key -match '(?i)(Duration)$') { $duration = $val } if (-not $width -and $key -match '(?i)(ImageWidth|SourceImageWidth|VideoFrameWidth)$') { $width = $val } if (-not $height -and $key -match '(?i)(ImageHeight|SourceImageHeight|VideoFrameHeight)$') { $height = $val } if (-not $camera -and $key -match '(?i)(Model|CameraModelName)$') { $camera = $val } if (-not $dateTaken -and $key -match '(?i)(DateTimeOriginal|CreateDate)$') { $dateTaken = $val } if (-not $office.pages -and $key -match '(?i)(PageCount)$' -and $val -match '^\d+$') { $office.pages = $val; $office.pageMethod='ExifTool - PageCount' } } } foreach ($pair in (Get-PdfData $f).GetEnumerator()) { $specific[$pair.Key]=$pair.Value } foreach ($pair in (Get-ImageData $f).GetEnumerator()) { $specific[$pair.Key]=$pair.Value } foreach ($pair in (Get-ZipData $f).GetEnumerator()) { $specific[$pair.Key]=$pair.Value } foreach ($pair in (Get-TextData $f).GetEnumerator()) { $specific[$pair.Key]=$pair.Value } if ($office.sheets) { $specific['Hojas']=$office.sheets } if ($office.slides) { $specific['Diapositivas']=$office.slides } if ($office.words) { $specific['Palabras']=$office.words } if ($office.characters) { $specific['Caracteres']=$office.characters } if ($office.paragraphs) { $specific['Parrafos']=$office.paragraphs } $result.Add([pscustomobject]@{ relativePath = $relative name = $f.Name extension = $f.Extension sizeBytes = $f.Length fileSystemCreation = $f.CreationTime.ToString('yyyy-MM-ddTHH:mm:sszzz') modified = $f.LastWriteTime.ToString('yyyy-MM-ddTHH:mm:sszzz') author = $authors title = $title subject = $subject keywords = $keywords contentCreated = $contentCreated lastPrinted = $lastPrinted pages = $office.pages pageMethod = $office.pageMethod duration = $duration dimensions = $dimensions width = $width height = $height bitrate = $bitrate dateTaken = $dateTaken camera = $camera officeProperties = $office.officeProperties formatSpecific = $specific exifToolProperties = $exifData shellProperties = $props }) } $payload = [ordered]@{ schema = 'gestion-documental-sena-windows-v23.0' root = $Ruta generatedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:sszzz') readOnly = $true tools = [ordered]@{ exifToolAvailable = [bool]($null -ne $exifToolCmd) exifToolPath = if ($exifToolCmd) { $exifToolCmd.Source } else { '' } droidAvailable = [bool]($null -ne $droidCmd) droidPath = if ($droidCmd) { $droidCmd.Source } else { '' } note = 'ExifTool se usa automaticamente si esta disponible. DROID/PRONOM se reporta como herramienta complementaria para identificacion normalizada de formatos.' } files = $result } $payload | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $Salida -Encoding UTF8 try { if ($script:word) { $script:word.Quit(); [void][Runtime.InteropServices.Marshal]::ReleaseComObject($script:word) } } catch {} try { if ($script:excel) { $script:excel.Quit(); [void][Runtime.InteropServices.Marshal]::ReleaseComObject($script:excel) } } catch {} try { if ($script:powerpoint) { $script:powerpoint.Quit(); [void][Runtime.InteropServices.Marshal]::ReleaseComObject($script:powerpoint) } } catch {} try { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($shell) } catch {} [GC]::Collect(); [GC]::WaitForPendingFinalizers() Write-Progress -Activity 'Extrayendo metadatos Windows' -Completed Write-Host "`nProceso finalizado. JSON generado en:" -ForegroundColor Green Write-Host $Salida Write-Host 'No se elimino ni modifico ningun documento.' -ForegroundColor Cyan