traslado 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
freoa_v6/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 Ricardo Peres Freoa
3
+ """Compatibilidade de módulos com Traslado durante a série 2.x."""
4
+ import importlib
5
+ import importlib.abc
6
+ import importlib.util
7
+ import sys
8
+
9
+
10
+ class _AliasLoader(importlib.abc.Loader):
11
+ def __init__(self, target):
12
+ self.target = target
13
+
14
+ def create_module(self, spec):
15
+ module = importlib.import_module(self.target)
16
+ self.spec = module.__spec__
17
+ return module
18
+
19
+ def exec_module(self, module):
20
+ module.__spec__ = self.spec
21
+
22
+ def get_filename(self, fullname):
23
+ # runpy sets argv[0] from spec.origin; argparse on Python 3.11-3.13
24
+ # requires a filename there, even when the code comes from an alias.
25
+ return importlib.util.find_spec(self.target).origin
26
+
27
+ def get_code(self, fullname):
28
+ # runpy requests code without create_module/exec_module. Execute the
29
+ # canonical module as __main__, keeping its relative imports canonical.
30
+ return compile(
31
+ "import sys, runpy\n"
32
+ "if hasattr(sys.stderr, 'reconfigure'):\n"
33
+ " sys.stderr.reconfigure(encoding='utf-8', errors='backslashreplace')\n"
34
+ "print('Depreciação: use python -m traslado em vez de freoa_v6.', file=sys.stderr)\n"
35
+ f"runpy.run_module({self.target!r}, run_name='__main__', alter_sys=True)\n",
36
+ self.get_filename(fullname), "exec", dont_inherit=True)
37
+
38
+
39
+ class _AliasFinder(importlib.abc.MetaPathFinder):
40
+ def find_spec(self, fullname, path=None, target=None):
41
+ if not fullname.startswith('freoa_v6.'):
42
+ return None
43
+ canonical = 'traslado' + fullname[len('freoa_v6'):]
44
+ spec = importlib.util.find_spec(canonical)
45
+ if spec is None:
46
+ return None
47
+ return importlib.util.spec_from_loader(
48
+ fullname, _AliasLoader(canonical),
49
+ is_package=spec.submodule_search_locations is not None)
50
+
51
+
52
+ sys.meta_path.insert(0, _AliasFinder())
53
+ sys.modules[__name__] = importlib.import_module('traslado')
traslado/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 Ricardo Peres Freoa
3
+ """Traslado. No model loading or network side effects on import."""
4
+ __version__ = "2.0.0"
traslado/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 Ricardo Peres Freoa
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
traslado/advanced.py ADDED
@@ -0,0 +1,179 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 Ricardo Peres Freoa
3
+ """Experimental opt-in bridge to separately installed MinerU Kit 4.0.10.
4
+
5
+ No engine/model is bundled. The bridge is a process boundary, not an OS sandbox.
6
+ HF offline variables prevent normal hub downloads, not arbitrary third-party IO.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import html
11
+ from html.parser import HTMLParser
12
+ import os
13
+ from pathlib import Path
14
+ import re
15
+ import tempfile
16
+
17
+ from .formats import image_to_png
18
+ from .model import Block, Document, Options
19
+ from .safety import is_link, run_bounded
20
+
21
+
22
+ class PassiveHTML(HTMLParser):
23
+ def __init__(self):
24
+ super().__init__(convert_charrefs=False)
25
+ self.out = []
26
+
27
+ def handle_starttag(self, tag, attrs):
28
+ if tag in {"table", "thead", "tbody", "tr", "td", "th", "br"}:
29
+ safe = []
30
+ for key, value in attrs:
31
+ if key in {"colspan", "rowspan"} and value and value.isdigit() and 1 <= int(value) <= 1000:
32
+ safe.append(f' {key}="{int(value)}"')
33
+ self.out.append("<" + tag + "".join(safe) + ">")
34
+ else:
35
+ self.out.append(html.escape(self.get_starttag_text()))
36
+
37
+ def handle_endtag(self, tag):
38
+ self.out.append("</" + tag + ">" if tag in {"table", "thead", "tbody", "tr", "td", "th"} else html.escape("</" + tag + ">"))
39
+
40
+ def handle_startendtag(self, tag, attrs):
41
+ # HTMLParser's default invents an end tag, including a visible escaped
42
+ # </br> for <br/>. Keep void breaks canonical and other safe tags closed
43
+ # with their original self-closing form; unknown tags remain literal.
44
+ if tag in {"table", "thead", "tbody", "tr", "td", "th", "br"}:
45
+ self.handle_starttag(tag, attrs)
46
+ if tag != "br":
47
+ self.out[-1] = self.out[-1][:-1] + "/>"
48
+ else:
49
+ self.out.append(html.escape(self.get_starttag_text()))
50
+
51
+ def handle_data(self, data):
52
+ self.out.append(html.escape(data, quote=False))
53
+
54
+ def handle_comment(self, data):
55
+ self.out.append(html.escape("<!--" + data + "-->"))
56
+
57
+ def handle_decl(self, decl):
58
+ self.out.append(html.escape("<!" + decl + ">"))
59
+
60
+ def handle_pi(self, data):
61
+ self.out.append(html.escape("<?" + data + ">"))
62
+
63
+ def unknown_decl(self, data):
64
+ self.out.append(html.escape("<![" + data + "]>"))
65
+
66
+ def handle_entityref(self, name):
67
+ self.out.append("&" + name + ";")
68
+
69
+ def handle_charref(self, name):
70
+ self.out.append("&#" + name + ";")
71
+
72
+
73
+ def sanitize(markdown: str, model_root: Path, workspace: Path, page_number: int, options: Options) -> str:
74
+ """Literalize model link syntax; retain only our reconstructed local images.
75
+
76
+ This deliberately sacrifices Markdown link/reference formatting instead of
77
+ trying to recognize CommonMark with regular expressions. Nothing is deleted
78
+ merely because it resembles a reference definition. The narrow image regex
79
+ is an optional importer, never a security filter: unmatched syntax remains
80
+ literal. Markers cannot collide with raw or entity-decoded model text.
81
+ """
82
+ image_count = 0
83
+ marker_prefix = "FREOAASSETMARKER0X"
84
+ decoded = html.unescape(markdown)
85
+ marker_number = 0
86
+ while marker_prefix in markdown or marker_prefix in decoded:
87
+ marker_number += 1
88
+ marker_prefix = f"FREOAASSETMARKER{marker_number}X"
89
+ markers = {}
90
+ root = model_root.resolve()
91
+
92
+ def local_image(match):
93
+ nonlocal image_count
94
+ relative = match.group(1)
95
+ parts = Path(relative).parts
96
+ if (not options.images or not re.fullmatch(r"[a-zA-Z0-9_.\-/]+", relative)
97
+ or relative.startswith("/") or any(p in {".", ".."} for p in parts)):
98
+ return match.group(0)
99
+ file = root / relative
100
+ # Check before resolving, including every intermediate directory.
101
+ if any(is_link(p) for p in [model_root, file, *file.parents] if p == root or root in p.parents or p == model_root):
102
+ return match.group(0)
103
+ file = file.resolve()
104
+ if root not in file.parents or not file.is_file() or file.stat().st_size > options.max_output_bytes:
105
+ return match.group(0)
106
+ image_count += 1
107
+ target = workspace / "assets" / f"advanced-{page_number}-{image_count}.png"
108
+ try:
109
+ image_to_png(file, target, options)
110
+ except (OSError, ValueError):
111
+ target.unlink(missing_ok=True)
112
+ raise
113
+ marker = f"{marker_prefix}{image_count}END"
114
+ markers[marker] = f"![Imagem reconstruída](assets/{target.name})"
115
+ return marker
116
+
117
+ markdown = re.sub(r'!\[[^\]\n]*\]\(([^)\n]*)\)', local_image, markdown)
118
+ parser = PassiveHTML()
119
+ parser.feed(markdown)
120
+ parser.close()
121
+ passive = "".join(parser.out).replace("[", "&#91;").replace("]", "&#93;")
122
+ for marker, image in markers.items():
123
+ passive = passive.replace(marker, image)
124
+ return passive
125
+
126
+
127
+ def refine(source: Path, workspace: Path, options: Options, document: Document) -> None:
128
+ if not options.advanced_kit:
129
+ return
130
+ executable = Path(options.advanced_kit)
131
+ if not executable.is_absolute() or not executable.is_file():
132
+ raise ValueError("Informe caminho absoluto de mineru-kit instalado separadamente")
133
+ import fitz
134
+ targets = [p for p in document.pages if p.route == "ocr" or any(
135
+ w.startswith(("region_ocr_failed", "pdf_table_merged", "reading_order_two_column", "table_detection_failed")) for w in p.warnings)]
136
+ if not targets:
137
+ return
138
+ env = os.environ.copy()
139
+ env.update(HF_HUB_OFFLINE="1", TRANSFORMERS_OFFLINE="1", HF_DATASETS_OFFLINE="1")
140
+ with tempfile.TemporaryDirectory(prefix="advanced-", dir=workspace) as name:
141
+ temporary = Path(name)
142
+ version_log = temporary / "version.log"
143
+ code = run_bounded([str(executable), "--version"], temporary, 10, version_log, env=env)
144
+ version = version_log.read_text(encoding="utf-8", errors="replace")
145
+ if code != 0 or not re.search(r"(?<![\d.])4\.0\.10(?![\d.])", version):
146
+ document.warnings.append("advanced_unsupported_version_expected_4.0.10")
147
+ return
148
+ with fitz.open(source) as pdf:
149
+ for page in targets:
150
+ folder = temporary / f"page-{page.number}"
151
+ folder.mkdir()
152
+ selected = folder / "selected.pdf"
153
+ with fitz.open() as one:
154
+ one.insert_pdf(pdf, from_page=page.number-1, to_page=page.number-1)
155
+ one.save(selected)
156
+ output = folder / "result.md"
157
+ try:
158
+ code = run_bounded([str(executable), "parse", str(selected), "-o", str(output),
159
+ "--tier", "advanced"], folder, options.advanced_timeout, folder / "parse.log", env=env)
160
+ if code != 0 or not output.is_file() or output.stat().st_size > options.max_output_bytes:
161
+ raise RuntimeError("MinerU não produziu resultado válido; mantenha modelos locais preparados")
162
+ content = output.read_text(encoding="utf-8")
163
+ if not content.strip():
164
+ raise RuntimeError("MinerU produziu resultado vazio")
165
+ passive = sanitize(content, folder, workspace, page.number, options)
166
+ # Keep previous extraction as an explicit alternative, never erase provenance.
167
+ # T11: line geometry is internal (never serialized); role only when classified.
168
+ previous = [{k: v for k, v in b.__dict__.items()
169
+ if k != "lines" and not (k == "role" and v is None)} for b in page.blocks]
170
+ page.blocks = [Block("text", passive, engine="mineru-kit-4.0.10",
171
+ details={"tier": "advanced", "previous_blocks": previous,
172
+ "source_page": page.number, "model_revision": "unverified",
173
+ "raw_model_markdown": content,
174
+ "markdown_policy": "literal_links_and_references_local_images_only"})]
175
+ page.route = "advanced"
176
+ page.warnings.append("model_reconstruction_requires_source_review")
177
+ page.warnings.append("model_markdown_links_literalized_formatting_partial")
178
+ except (RuntimeError, TimeoutError, ValueError, OSError) as exc:
179
+ page.warnings.append("advanced_failed: " + str(exc))
@@ -0,0 +1,303 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 Ricardo Peres Freoa
3
+ [CmdletBinding()]
4
+ param(
5
+ [string]$Destination,
6
+ [string[]]$Languages = @('por', 'eng', 'spa', 'ita', 'fra', 'deu'),
7
+ [string]$CacheDirectory,
8
+ [string]$LockPath,
9
+ [switch]$Offline
10
+ )
11
+
12
+ # Extract authenticated official archives; never execute their installers.
13
+ # The caller supplies a new directory inside its unpublished release stage.
14
+ Set-StrictMode -Version Latest
15
+ $ErrorActionPreference = 'Stop'
16
+ if (-not $LockPath) { $LockPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'ocr-assets.lock.json' }
17
+
18
+ function Get-OcrReparseTag([string]$Path) {
19
+ if (-not ('FreoaOcrReparseNative' -as [type])) {
20
+ Add-Type -TypeDefinition @'
21
+ using System;
22
+ using System.ComponentModel;
23
+ using System.Runtime.InteropServices;
24
+ using Microsoft.Win32.SafeHandles;
25
+ public static class FreoaOcrReparseNative {
26
+ [StructLayout(LayoutKind.Sequential)]
27
+ private struct AttributeTagInfo { public uint Attributes; public uint Tag; }
28
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
29
+ private static extern SafeFileHandle CreateFileW(string name, uint access, uint share,
30
+ IntPtr security, uint disposition, uint flags, IntPtr template);
31
+ [DllImport("kernel32.dll", SetLastError=true)]
32
+ private static extern bool GetFileInformationByHandleEx(SafeFileHandle handle,
33
+ int informationClass, out AttributeTagInfo info, uint size);
34
+ public static uint GetTag(string path) {
35
+ // Query the placeholder itself; never follow a name-surrogate link.
36
+ using (SafeFileHandle handle = CreateFileW(path, 0, 7, IntPtr.Zero, 3,
37
+ 0x02200000, IntPtr.Zero)) {
38
+ if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
39
+ AttributeTagInfo info;
40
+ if (!GetFileInformationByHandleEx(handle, 9, out info, 8))
41
+ throw new Win32Exception(Marshal.GetLastWin32Error());
42
+ return info.Tag;
43
+ }
44
+ }
45
+ }
46
+ '@
47
+ }
48
+ return [FreoaOcrReparseNative]::GetTag($Path)
49
+ }
50
+
51
+ function Test-OcrCloudTag([uint32]$Tag) {
52
+ # Microsoft CLOUD and CLOUD_1..F differ only in bits 12..15.
53
+ # All other tags, including symlink/junction/name-surrogate tags, fail closed.
54
+ return (($Tag -band [uint32]4294905855) -eq [uint32]2415919130)
55
+ }
56
+
57
+ function Assert-OcrNoLinks([string]$Path) {
58
+ $cursor = [IO.Path]::GetFullPath($Path)
59
+ while ($cursor) {
60
+ if (Test-Path -LiteralPath $cursor) {
61
+ $item = Get-Item -LiteralPath $cursor -Force
62
+ if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and
63
+ -not (Test-OcrCloudTag (Get-OcrReparseTag $cursor))) {
64
+ throw "OCR: links/junctions nao permitidos: $cursor"
65
+ }
66
+ } else {
67
+ # Get-Item also finds dangling links which Test-Path may omit.
68
+ $item = Get-Item -LiteralPath $cursor -Force -ErrorAction SilentlyContinue
69
+ if ($item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and
70
+ -not (Test-OcrCloudTag (Get-OcrReparseTag $cursor))) {
71
+ throw "OCR: link pendente nao permitido: $cursor"
72
+ }
73
+ }
74
+ $parent = [IO.Path]::GetDirectoryName($cursor)
75
+ if ($parent -eq $cursor) { break }
76
+ $cursor = $parent
77
+ }
78
+ }
79
+
80
+ function Assert-OcrMemberName([string]$Name) {
81
+ if ($Name -notmatch '^[A-Za-z0-9_.+-]+(?:/[A-Za-z0-9_.+-]+)*$' -or
82
+ @($Name.Split('/') | Where-Object { $_ -eq '.' -or $_ -eq '..' -or $_.StartsWith('-') }).Count) {
83
+ throw "OCR: membro invalido no lock: $Name"
84
+ }
85
+ }
86
+
87
+ function Get-OcrLockedFile([string]$Path, $Specification) {
88
+ Assert-OcrNoLinks $Path
89
+ $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
90
+ try {
91
+ if ($stream.Length -ne [long]$Specification.bytes) { throw "OCR: tamanho divergente: $Path" }
92
+ $algorithm = [Security.Cryptography.SHA256]::Create()
93
+ try { $actual = [BitConverter]::ToString($algorithm.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() }
94
+ finally { $algorithm.Dispose() }
95
+ if ($actual -ne $Specification.sha256) { throw "OCR: SHA256 divergente: $Path" }
96
+ $stream.Position = 0
97
+ return [pscustomobject]@{ Path = $Path; Handle = $stream }
98
+ } catch { $stream.Dispose(); throw }
99
+ }
100
+
101
+ function Save-OcrPinnedAsset($Asset, [string]$Cache, [bool]$OnlyOffline) {
102
+ Assert-OcrMemberName $Asset.name
103
+ if ($Asset.name.Contains('/')) { throw 'OCR: nome de asset deve ser simples' }
104
+ if ($Asset.sha256 -notmatch '^[a-f0-9]{64}$' -or [long]$Asset.bytes -lt 1 -or [long]$Asset.bytes -gt 100000000) {
105
+ throw 'OCR: hash/tamanho invalido no lock'
106
+ }
107
+ $target = Join-Path $Cache $Asset.name
108
+ if (Test-Path -LiteralPath $target) { return Get-OcrLockedFile $target $Asset }
109
+ if ($OnlyOffline) { throw "OCR: cache offline incompleto: $($Asset.name)" }
110
+ $allowed = @('github.com', 'raw.githubusercontent.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com')
111
+ $uri = [Uri]$Asset.url
112
+ if ($uri.Scheme -ne 'https' -or $uri.Host -notin $allowed -or $uri.UserInfo) { throw 'OCR: origem HTTPS invalida' }
113
+ Add-Type -AssemblyName System.Net.Http
114
+ $handler = New-Object Net.Http.HttpClientHandler
115
+ $handler.AllowAutoRedirect = $false
116
+ $client = New-Object Net.Http.HttpClient($handler)
117
+ $client.Timeout = [TimeSpan]::FromSeconds(90)
118
+ $client.DefaultRequestHeaders.UserAgent.ParseAdd('Freoa-Converter-OCR/6.0rc3')
119
+ $partial = Join-Path $Cache ('.download-' + [Guid]::NewGuid().ToString('N') + '.part')
120
+ $response = $null
121
+ $output = $null
122
+ try {
123
+ for ($redirect = 0; $redirect -le 5; $redirect++) {
124
+ if ($uri.Scheme -ne 'https' -or $uri.Host -notin $allowed -or $uri.UserInfo) { throw 'OCR: redirect HTTPS invalido' }
125
+ $response = $client.GetAsync($uri, [Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
126
+ $code = [int]$response.StatusCode
127
+ if ($code -in @(301, 302, 303, 307, 308)) {
128
+ if ($redirect -eq 5 -or -not $response.Headers.Location) { throw 'OCR: excesso de redirects' }
129
+ $uri = New-Object Uri($uri, $response.Headers.Location)
130
+ $response.Dispose(); $response = $null
131
+ continue
132
+ }
133
+ $response.EnsureSuccessStatusCode() | Out-Null
134
+ break
135
+ }
136
+ $contentLength = $response.Content.Headers.ContentLength
137
+ if ($contentLength -and $contentLength -ne [long]$Asset.bytes) { throw 'OCR: tamanho HTTP diverge do lock' }
138
+ $input = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
139
+ try {
140
+ $output = [IO.File]::Open($partial, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
141
+ $buffer = New-Object byte[] 65536
142
+ [long]$total = 0
143
+ $deadline = [DateTime]::UtcNow.AddSeconds(180)
144
+ while ($true) {
145
+ $readTask = $input.ReadAsync($buffer, 0, $buffer.Length)
146
+ if (-not $readTask.Wait(90000)) { throw 'OCR: download sem progresso no prazo' }
147
+ $read = $readTask.GetAwaiter().GetResult()
148
+ if ($read -eq 0) { break }
149
+ $total += $read
150
+ if ($total -gt [long]$Asset.bytes -or [DateTime]::UtcNow -gt $deadline) { throw 'OCR: download excede limite/prazo' }
151
+ $output.Write($buffer, 0, $read)
152
+ }
153
+ $output.Dispose(); $output = $null
154
+ } finally { $input.Dispose() }
155
+ $checked = Get-OcrLockedFile $partial $Asset
156
+ $checked.Handle.Dispose()
157
+ # No overwrite: another bootstrap may have populated this cache meanwhile.
158
+ [IO.File]::Move($partial, $target)
159
+ return Get-OcrLockedFile $target $Asset
160
+ } finally {
161
+ if ($output) { $output.Dispose() }
162
+ if ($response) { $response.Dispose() }
163
+ $client.Dispose(); $handler.Dispose()
164
+ if (Test-Path -LiteralPath $partial) { Remove-Item -LiteralPath $partial -Force }
165
+ }
166
+ }
167
+
168
+ function Invoke-OcrPrivateTool([string]$Executable, [string[]]$Arguments, [string]$WorkingDirectory, [int]$TimeoutSeconds = 60) {
169
+ $info = New-Object Diagnostics.ProcessStartInfo
170
+ $info.FileName = $Executable
171
+ # Windows file names cannot contain quotes. None of our arguments ends in a
172
+ # backslash; paths are complete files or non-root directory names.
173
+ if (@($Arguments | Where-Object { $_.Contains('"') -or $_.EndsWith('\') }).Count) { throw 'OCR: argumento nativo invalido' }
174
+ $info.Arguments = (($Arguments | ForEach-Object { '"' + $_ + '"' }) -join ' ')
175
+ $info.WorkingDirectory = $WorkingDirectory
176
+ $info.UseShellExecute = $false; $info.CreateNoWindow = $true
177
+ $info.RedirectStandardOutput = $true; $info.RedirectStandardError = $true
178
+ $info.EnvironmentVariables['TESSDATA_PREFIX'] = (Join-Path $WorkingDirectory 'tessdata')
179
+ $process = New-Object Diagnostics.Process
180
+ $process.StartInfo = $info
181
+ try {
182
+ if (-not $process.Start()) { throw 'OCR: ferramenta nao iniciou' }
183
+ $stdout = $process.StandardOutput.ReadToEndAsync()
184
+ $stderr = $process.StandardError.ReadToEndAsync()
185
+ if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
186
+ # Only our newly-created private process tree; no installer execution.
187
+ & "$env:SystemRoot\System32\taskkill.exe" /PID $process.Id /T /F 2>&1 | Out-Null
188
+ $process.WaitForExit(5000) | Out-Null
189
+ throw 'OCR: ferramenta excedeu prazo'
190
+ }
191
+ $out = $stdout.GetAwaiter().GetResult(); $err = $stderr.GetAwaiter().GetResult()
192
+ if ($out.Length + $err.Length -gt 1000000) { throw 'OCR: log de ferramenta excedeu limite' }
193
+ if ($process.ExitCode -ne 0) { throw "OCR: ferramenta falhou ($($process.ExitCode)): $err" }
194
+ return [pscustomobject]@{ ExitCode = $process.ExitCode; Stdout = $out; Stderr = $err }
195
+ } finally { $process.Dispose() }
196
+ }
197
+
198
+ function Install-FreoaOcr {
199
+ [CmdletBinding()]
200
+ param([Parameter(Mandatory)][string]$Destination,
201
+ [string[]]$Languages = @('por', 'eng', 'spa', 'ita', 'fra', 'deu'),
202
+ [string]$CacheDirectory, [Parameter(Mandatory)][string]$LockPath, [switch]$Offline)
203
+ if ($env:OS -ne 'Windows_NT' -or -not [Environment]::Is64BitOperatingSystem) { throw 'OCR: requer Windows x64' }
204
+ if (-not [IO.Path]::IsPathRooted($Destination)) { throw 'OCR: destino deve ser absoluto' }
205
+ $destinationFull = [IO.Path]::GetFullPath($Destination).TrimEnd('\', '/')
206
+ $parent = [IO.Path]::GetDirectoryName($destinationFull)
207
+ if (-not $parent -or -not [IO.Directory]::Exists($parent) -or $destinationFull -eq [IO.Path]::GetPathRoot($destinationFull).TrimEnd('\')) {
208
+ throw 'OCR: destino requer pai existente e diretorio novo'
209
+ }
210
+ Assert-OcrNoLinks $destinationFull
211
+ if (Get-Item -LiteralPath $destinationFull -Force -ErrorAction SilentlyContinue) { throw 'OCR: destino ja existe; nao reutilizado' }
212
+ $lockBytes = [IO.File]::ReadAllBytes($LockPath)
213
+ if ($lockBytes.Length -gt 1000000) { throw 'OCR: lock excede limite' }
214
+ $lockText = [Text.Encoding]::UTF8.GetString($lockBytes).TrimStart([char]0xFEFF)
215
+ $lock = $lockText | ConvertFrom-Json
216
+ $lockAlgorithm = [Security.Cryptography.SHA256]::Create()
217
+ try { $lockHash = [BitConverter]::ToString($lockAlgorithm.ComputeHash($lockBytes)).Replace('-', '').ToLowerInvariant() }
218
+ finally { $lockAlgorithm.Dispose() }
219
+ if ($lock.schema_version -ne 1 -or $lock.platform -ne 'windows-x64' -or $lock.engine_version -ne '5.5.3') { throw 'OCR: lock incompativel' }
220
+ # Python/native argv transports arrays as one comma/plus-separated value.
221
+ $Languages = @($Languages | ForEach-Object { $_ -split '[,+]' } | ForEach-Object { $_.Trim().ToLowerInvariant() })
222
+ if (-not $Languages.Count -or @($Languages | Select-Object -Unique).Count -ne $Languages.Count -or @($Languages | Where-Object { $_ -notin $lock.default_languages }).Count) {
223
+ throw 'OCR: idiomas invalidos; use por, eng, spa, ita, fra ou deu'
224
+ }
225
+ foreach ($member in @($lock.seven_zip_members) + @($lock.tesseract_members)) { Assert-OcrMemberName $member.path }
226
+ $cache = if ($CacheDirectory) { [IO.Path]::GetFullPath($CacheDirectory) } else { Join-Path $parent '.ocr-download-cache' }
227
+ if ($cache -eq $destinationFull -or $cache.StartsWith($destinationFull + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'OCR: cache deve ficar fora do destino' }
228
+ Assert-OcrNoLinks $cache
229
+ if (-not [IO.Directory]::Exists($cache)) { New-Item -ItemType Directory -Path $cache | Out-Null }
230
+ $held = New-Object Collections.Generic.List[object]
231
+ $created = $false
232
+ try {
233
+ $assets = @{}
234
+ $wanted = @('tesseract-ocr-w64-setup-5.5.3.20260724.exe', '7zr.exe', '7z2603-x64.exe', 'TESSDATA-LICENSE.txt') + @($Languages | ForEach-Object { $_ + '.traineddata' })
235
+ foreach ($name in $wanted) {
236
+ $matches = @($lock.assets | Where-Object { $_.name -eq $name })
237
+ if ($matches.Count -ne 1) { throw "OCR: asset unico ausente no lock: $name" }
238
+ $asset = Save-OcrPinnedAsset $matches[0] $cache $Offline.IsPresent
239
+ $held.Add($asset); $assets[$name] = $asset.Path
240
+ }
241
+ # Reserve atomically after all downloads verified; any preexisting path fails.
242
+ New-Item -ItemType Directory -Path $destinationFull -ErrorAction Stop | Out-Null
243
+ $created = $true
244
+ $scratch = Join-Path $destinationFull '.bootstrap-tools'
245
+ New-Item -ItemType Directory -Path $scratch | Out-Null
246
+ $privateInputs = Join-Path $scratch 'inputs'
247
+ New-Item -ItemType Directory -Path $privateInputs | Out-Null
248
+ $privateAssets = @{}
249
+ foreach ($name in @('7zr.exe', '7z2603-x64.exe', 'tesseract-ocr-w64-setup-5.5.3.20260724.exe')) {
250
+ # Read from the authenticated, write/delete-denying handle. Native
251
+ # programs only consume fresh private copies, never OneDrive inputs.
252
+ $source = @($held | Where-Object { $_.Path -eq $assets[$name] })[0]
253
+ $copy = Join-Path $privateInputs $name
254
+ $output = [IO.File]::Open($copy, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
255
+ try { $source.Handle.Position = 0; $source.Handle.CopyTo($output) }
256
+ finally { $output.Dispose(); $source.Handle.Position = 0 }
257
+ $specification = @($lock.assets | Where-Object { $_.name -eq $name })[0]
258
+ $bound = Get-OcrLockedFile $copy $specification
259
+ $held.Add($bound); $privateAssets[$name] = $copy
260
+ }
261
+ $sevenArgs = @('x', $privateAssets['7z2603-x64.exe'], '-y', '-bd', '-bso0', '-bsp0', ('-o' + $scratch), '--') + @($lock.seven_zip_members | ForEach-Object { $_.path })
262
+ Invoke-OcrPrivateTool $privateAssets['7zr.exe'] $sevenArgs $scratch | Out-Null
263
+ foreach ($member in $lock.seven_zip_members) { $bound = Get-OcrLockedFile (Join-Path $scratch $member.path) $member; $held.Add($bound) }
264
+ $engineArgs = @('x', $privateAssets['tesseract-ocr-w64-setup-5.5.3.20260724.exe'], '-y', '-bd', '-bso0', '-bsp0', ('-o' + $destinationFull), '--') + @($lock.tesseract_members | ForEach-Object { $_.path })
265
+ Invoke-OcrPrivateTool (Join-Path $scratch '7z.exe') $engineArgs $scratch | Out-Null
266
+ foreach ($member in $lock.tesseract_members) { $bound = Get-OcrLockedFile (Join-Path $destinationFull $member.path) $member; $held.Add($bound) }
267
+ foreach ($language in $Languages) { Copy-Item -LiteralPath $assets[($language + '.traineddata')] -Destination (Join-Path $destinationFull ('tessdata/' + $language + '.traineddata')) }
268
+ Copy-Item -LiteralPath $assets['TESSDATA-LICENSE.txt'] -Destination (Join-Path $destinationFull 'doc/TESSDATA-LICENSE.txt')
269
+ Copy-Item -LiteralPath (Join-Path $scratch 'License.txt') -Destination (Join-Path $destinationFull 'doc/7ZIP-LICENSE.txt')
270
+ foreach ($language in $Languages) {
271
+ $spec = @($lock.assets | Where-Object { $_.name -eq ($language + '.traineddata') })[0]
272
+ $bound = Get-OcrLockedFile (Join-Path $destinationFull ('tessdata/' + $language + '.traineddata')) $spec; $held.Add($bound)
273
+ }
274
+ $exe = Join-Path $destinationFull 'tesseract.exe'; $tessdata = Join-Path $destinationFull 'tessdata'
275
+ $version = Invoke-OcrPrivateTool $exe @('--version') $destinationFull 15
276
+ if ($version.Stdout -notmatch '(?m)^tesseract v?5\.5\.3(?:\.|\s|$)') { throw 'OCR: versao inesperada' }
277
+ $listed = Invoke-OcrPrivateTool $exe @('--tessdata-dir', $tessdata, '--list-langs') $destinationFull 15
278
+ $available = @($listed.Stdout -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^[a-z]{3}$' })
279
+ if (@($Languages | Where-Object { $_ -notin $available }).Count) { throw 'OCR: idioma instalado nao listado pelo motor' }
280
+ $receipt = [ordered]@{ schema_version = 1; version = $version.Stdout.Split("`n")[0].Trim(); languages = $Languages; lock_sha256 = $lockHash; installers_executed = $false; global_environment_changed = $false }
281
+ $receipt | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $destinationFull 'ocr-runtime.json') -Encoding UTF8
282
+ foreach ($entry in $held) { $entry.Handle.Dispose() }; $held.Clear()
283
+ Assert-OcrNoLinks $scratch
284
+ if ([IO.Path]::GetFullPath($scratch) -ne (Join-Path $destinationFull '.bootstrap-tools')) { throw 'OCR: cleanup fora do stage' }
285
+ Remove-Item -LiteralPath $scratch -Recurse -Force
286
+ return [pscustomobject]@{ Tesseract = $exe; Tessdata = $tessdata; Languages = $Languages; Version = $receipt.version; Ready = $true; CacheDirectory = $cache }
287
+ } catch {
288
+ foreach ($entry in $held) { $entry.Handle.Dispose() }; $held.Clear()
289
+ if ($created) {
290
+ # Only the directory this call reserved; never a reused installation.
291
+ Assert-OcrNoLinks $destinationFull
292
+ if ([IO.Path]::GetDirectoryName($destinationFull) -ne $parent) { throw 'OCR: cleanup fora do pai reservado' }
293
+ $links = @(Get-ChildItem -LiteralPath $destinationFull -Recurse -Force | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
294
+ if ($links.Count) { throw 'OCR: cleanup recusado por link inesperado; stage preservado' }
295
+ Remove-Item -LiteralPath $destinationFull -Recurse -Force
296
+ }
297
+ throw
298
+ } finally { foreach ($entry in $held) { $entry.Handle.Dispose() } }
299
+ }
300
+
301
+ if ($Destination) {
302
+ Install-FreoaOcr -Destination $Destination -Languages $Languages -CacheDirectory $CacheDirectory -LockPath $LockPath -Offline:$Offline
303
+ }
Binary file
@@ -0,0 +1,33 @@
1
+ %PDF-1.4
2
+ %����
3
+ 1 0 obj
4
+ << /Type /Catalog /Pages 2 0 R >>
5
+ endobj
6
+ 2 0 obj
7
+ << /Type /Pages /Kids [3 0 R] /Count 1 >>
8
+ endobj
9
+ 3 0 obj
10
+ << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>
11
+ endobj
12
+ 4 0 obj
13
+ << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
14
+ endobj
15
+ 5 0 obj
16
+ << /Length 73 >>
17
+ stream
18
+ BT /F1 12 Tf 72 720 Td (Traslado verificacao de instalacao PDF OK.) Tj ET
19
+ endstream
20
+ endobj
21
+ xref
22
+ 0 6
23
+ 0000000000 65535 f
24
+ 0000000015 00000 n
25
+ 0000000064 00000 n
26
+ 0000000121 00000 n
27
+ 0000000247 00000 n
28
+ 0000000317 00000 n
29
+ trailer
30
+ << /Size 6 /Root 1 0 R >>
31
+ startxref
32
+ 440
33
+ %%EOF