Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2679361bc | ||
|
|
20c7fd8813 |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.3.2"
|
version = "2.4.0"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ def browse():
|
|||||||
|
|
||||||
|
|
||||||
@bp.get("/<token>/herunterladen")
|
@bp.get("/<token>/herunterladen")
|
||||||
@permission_required("fileshare.view")
|
# Herunterladen traegt Daten aus dem System heraus -- in der Rechtematrix ist
|
||||||
|
# das "Exportieren", nicht "Ansehen". Eine Vorschau bleibt im Haus, eine
|
||||||
|
# heruntergeladene Datei nicht.
|
||||||
|
@permission_required("fileshare.export")
|
||||||
def download(token: str):
|
def download(token: str):
|
||||||
mount = _mount_or_404(token)
|
mount = _mount_or_404(token)
|
||||||
relative = request.args.get("pfad") or ""
|
relative = request.args.get("pfad") or ""
|
||||||
@@ -184,7 +187,7 @@ def upload(token: str):
|
|||||||
|
|
||||||
|
|
||||||
@bp.post("/<token>/formular")
|
@bp.post("/<token>/formular")
|
||||||
@permission_required("fileshare.create")
|
@permission_required("fileshare_pdf.create")
|
||||||
def save_form(token: str):
|
def save_form(token: str):
|
||||||
"""Nimmt ein in der Vorschau ausgefuelltes PDF und legt es daneben ab.
|
"""Nimmt ein in der Vorschau ausgefuelltes PDF und legt es daneben ab.
|
||||||
|
|
||||||
@@ -207,6 +210,17 @@ def save_form(token: str):
|
|||||||
# waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen.
|
# waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen.
|
||||||
return {"error": "Das ist keine PDF-Datei."}, 415
|
return {"error": "Das ist keine PDF-Datei."}, 415
|
||||||
|
|
||||||
|
# Geprueft wird die **Datei**, nicht ein mitgeschicktes Auswahlfeld: wer nur
|
||||||
|
# anlegen darf, legt endgueltige Fassungen ab. Traegt die Datei noch
|
||||||
|
# Formularfelder, ist sie weiter veraenderbar -- dafuer braucht es
|
||||||
|
# "PDF-Formulare: Bearbeiten". Ein umgebogenes Feld im Browser hilft damit
|
||||||
|
# niemandem weiter.
|
||||||
|
if service.hat_formularfelder(daten) and not current_user.has("fileshare_pdf.edit"):
|
||||||
|
return {
|
||||||
|
"error": "Sie duerfen ausgefuellte Formulare nur abgeschlossen speichern "
|
||||||
|
"(Felder gesperrt). Bitte im Auswahlfeld 'Abgeschlossen' waehlen."
|
||||||
|
}, 403
|
||||||
|
|
||||||
# Wohin und unter welchem Namen -- beides darf der Benutzer bestimmen.
|
# Wohin und unter welchem Namen -- beides darf der Benutzer bestimmen.
|
||||||
ordner = (request.form.get("ordner") or "").strip().strip("/")
|
ordner = (request.form.get("ordner") or "").strip().strip("/")
|
||||||
wunschname = (request.form.get("name") or "").strip()
|
wunschname = (request.form.get("name") or "").strip()
|
||||||
|
|||||||
@@ -85,11 +85,30 @@ DHCP = Resource(
|
|||||||
FILESHARE = Resource(
|
FILESHARE = Resource(
|
||||||
key="fileshare",
|
key="fileshare",
|
||||||
label="Dateifreigaben",
|
label="Dateifreigaben",
|
||||||
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE),
|
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.EXPORT),
|
||||||
hint="SMB-Freigaben aus dem Verzeichnisdienst (lizenzpflichtiges Modul)",
|
hint="SMB-Freigaben aus dem Verzeichnisdienst (lizenzpflichtiges Modul)",
|
||||||
action_hints={
|
action_hints={
|
||||||
A.CREATE: "Ordner anlegen und Dateien hochladen.",
|
A.CREATE: "Ordner anlegen und Dateien hochladen.",
|
||||||
A.EDIT: "Umbenennen und verschieben.",
|
A.EDIT: "Umbenennen und verschieben.",
|
||||||
|
# Genau die Bedeutung aus der Legende: Daten aus dem System
|
||||||
|
# heraustragen. Ansehen und Mitnehmen sind zweierlei -- eine Vorschau
|
||||||
|
# bleibt im Haus, eine heruntergeladene Datei nicht.
|
||||||
|
A.EXPORT: "Dateien herunterladen.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Eigene Zeile statt Anhaengsel von "Dateifreigaben": ein Formular auszufuellen
|
||||||
|
#: ist ein anderer Vorgang als eine Datei hochzuladen, und wer umbenennen darf,
|
||||||
|
#: soll deshalb nicht automatisch veraenderbare Formulare ablegen duerfen.
|
||||||
|
FILESHARE_PDF = Resource(
|
||||||
|
key="fileshare_pdf",
|
||||||
|
label="PDF-Formulare",
|
||||||
|
actions=(A.VIEW, A.CREATE, A.EDIT),
|
||||||
|
hint="Formulare in der Vorschau ausfuellen und in der Freigabe ablegen",
|
||||||
|
action_hints={
|
||||||
|
A.VIEW: "In der Vorschau ausfuellen (ohne Speichern).",
|
||||||
|
A.CREATE: "Ausgefuellt speichern -- abgeschlossen, Felder gesperrt.",
|
||||||
|
A.EDIT: "Ausgefuellt speichern -- weiter ausfuellbar, inklusive Bild.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,7 +138,7 @@ FILES_AREA = Area(
|
|||||||
key="files",
|
key="files",
|
||||||
label="Dateifreigaben",
|
label="Dateifreigaben",
|
||||||
icon="folder",
|
icon="folder",
|
||||||
resources=(FILESHARE,),
|
resources=(FILESHARE, FILESHARE_PDF),
|
||||||
)
|
)
|
||||||
|
|
||||||
PERMISSIONS = PermissionTree(
|
PERMISSIONS = PermissionTree(
|
||||||
|
|||||||
@@ -250,8 +250,32 @@ CREATE INDEX IF NOT EXISTS idx_restart_events_method ON restart_events(method, t
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Neue Rechte sind nach einem Update bei niemandem gesetzt -- ohne diesen
|
||||||
|
# Schritt koennte niemand mehr herunterladen oder ein Formular speichern,
|
||||||
|
# obwohl sich an den Gruppen nichts geaendert hat. Die Zuordnung bildet ab, was
|
||||||
|
# vorher galt.
|
||||||
|
V4 = """
|
||||||
|
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||||
|
SELECT group_id, 'fileshare.export' FROM group_permissions
|
||||||
|
WHERE permission = 'fileshare.view';
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||||
|
SELECT group_id, 'fileshare_pdf.view' FROM group_permissions
|
||||||
|
WHERE permission = 'fileshare.view';
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||||
|
SELECT group_id, 'fileshare_pdf.create' FROM group_permissions
|
||||||
|
WHERE permission = 'fileshare.create';
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||||
|
SELECT group_id, 'fileshare_pdf.edit' FROM group_permissions
|
||||||
|
WHERE permission = 'fileshare.edit';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||||
Migration(version=1, name="baseline", sql=V1),
|
Migration(version=1, name="baseline", sql=V1),
|
||||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||||
Migration(version=3, name="restart-events", sql=V3),
|
Migration(version=3, name="restart-events", sql=V3),
|
||||||
|
Migration(version=4, name="fileshare-pdf-rechte", sql=V4),
|
||||||
)
|
)
|
||||||
@@ -21,6 +21,7 @@ Sicherheitsrelevante Aenderungen gegenueber dem Vorgaenger:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import zlib
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
@@ -622,6 +623,29 @@ def all_directories(mount: Mount, *, tiefe: int = 4, grenze: int = 400) -> list[
|
|||||||
return gefunden
|
return gefunden
|
||||||
|
|
||||||
|
|
||||||
|
def hat_formularfelder(daten: bytes) -> bool:
|
||||||
|
"""Steckt in diesen PDF-Bytes noch ein ausfuellbares Formular?
|
||||||
|
|
||||||
|
Gesucht wird nach Formularfeldern (``/Subtype /Widget``) -- auch in
|
||||||
|
gepackten Objektstroemen, denn moderne Schreiber legen fast alles dort ab.
|
||||||
|
Ein flach gerechnetes Formular hat keine mehr.
|
||||||
|
|
||||||
|
Bewusst ohne Zusatzpaket: die Frage ist einfach genug, und eine
|
||||||
|
PDF-Bibliothek auf jedem Zielsystem waere ein hoher Preis dafuer.
|
||||||
|
"""
|
||||||
|
text = bytearray(daten)
|
||||||
|
for treffer in re.finditer(rb"stream\r?\n", daten):
|
||||||
|
start = treffer.end()
|
||||||
|
ende = daten.find(b"endstream", start)
|
||||||
|
if ende == -1:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text += b"\n" + zlib.decompress(daten[start:ende])
|
||||||
|
except zlib.error:
|
||||||
|
continue # kein zlib-Strom -- Bilder, Schriften, alles moegliche
|
||||||
|
return re.search(rb"/Subtype\s*/Widget", bytes(text)) is not None
|
||||||
|
|
||||||
|
|
||||||
def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path:
|
def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path:
|
||||||
"""Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``.
|
"""Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``.
|
||||||
|
|
||||||
|
|||||||
@@ -106,14 +106,22 @@
|
|||||||
data-fill-Kind="{{ entry.preview_kind }}"
|
data-fill-Kind="{{ entry.preview_kind }}"
|
||||||
data-fill-Pfad="{{ entry.relative }}"
|
data-fill-Pfad="{{ entry.relative }}"
|
||||||
data-fill-Name="{{ entry.name }}"
|
data-fill-Name="{{ entry.name }}"
|
||||||
data-fill-Download="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}">
|
{% if current_user.has('fileshare.export') %}data-fill-Download="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"{% endif %}>
|
||||||
{{ icon('file-text', size=15) }} {{ entry.name }}
|
{{ icon('file-text', size=15) }} {{ entry.name }}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="link-icon"
|
{% if current_user.has('fileshare.export') %}
|
||||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}">
|
<a class="link-icon"
|
||||||
{{ icon('file-text', size=15) }} {{ entry.name }}
|
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}">
|
||||||
</a>
|
{{ icon('file-text', size=15) }} {{ entry.name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{# Ohne Exportrecht bleibt der Name Text: es gibt hier
|
||||||
|
nichts zu holen, und ein toter Link waere Spott. #}
|
||||||
|
<span class="link-icon faint">
|
||||||
|
{{ icon('file-text', size=15) }} {{ entry.name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="numeric fs-sm">{{ entry.size | bytes }}</td>
|
<td class="numeric fs-sm">{{ entry.size | bytes }}</td>
|
||||||
@@ -126,7 +134,7 @@
|
|||||||
data-fill-Kind="{{ entry.preview_kind }}"
|
data-fill-Kind="{{ entry.preview_kind }}"
|
||||||
data-fill-Pfad="{{ entry.relative }}"
|
data-fill-Pfad="{{ entry.relative }}"
|
||||||
data-fill-Name="{{ entry.name }}"
|
data-fill-Name="{{ entry.name }}"
|
||||||
data-fill-Download="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"
|
{% if current_user.has('fileshare.export') %}data-fill-Download="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"{% endif %}
|
||||||
aria-label="Vorschau">{{ icon('eye', size=15) }}</button>
|
aria-label="Vorschau">{{ icon('eye', size=15) }}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.has('fileshare.edit') %}
|
{% if current_user.has('fileshare.edit') %}
|
||||||
@@ -137,9 +145,11 @@
|
|||||||
data-fill-Endung="{{ entry.suffix or '' }}"
|
data-fill-Endung="{{ entry.suffix or '' }}"
|
||||||
aria-label="Umbenennen">{{ icon('pencil', size=15) }}</button>
|
aria-label="Umbenennen">{{ icon('pencil', size=15) }}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn--sm btn--ghost"
|
{% if current_user.has('fileshare.export') %}
|
||||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"
|
<a class="btn btn--sm btn--ghost"
|
||||||
aria-label="Herunterladen">{{ icon('download', size=15) }}</a>
|
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"
|
||||||
|
aria-label="Herunterladen">{{ icon('download', size=15) }}</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -210,6 +220,9 @@
|
|||||||
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
||||||
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
||||||
<div class="modal__body modal__body--flush" data-viewer
|
<div class="modal__body modal__body--flush" data-viewer
|
||||||
|
{# Ohne das Recht zum Ausfuellen zeigt die Vorschau das Dokument so,
|
||||||
|
wie es ist -- mit seinen eigenen Marken, ohne Eingabefelder. #}
|
||||||
|
data-viewer-formulare="{{ '1' if current_user.has('fileshare_pdf.view') else '' }}"
|
||||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}"
|
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}"
|
||||||
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}">
|
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}">
|
||||||
<div class="viewer">
|
<div class="viewer">
|
||||||
@@ -227,7 +240,9 @@
|
|||||||
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
||||||
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
||||||
{# Gefragt wird erst, wenn gespeichert werden soll -- vorher waere es
|
{# Gefragt wird erst, wenn gespeichert werden soll -- vorher waere es
|
||||||
eine Zeile Formular ueber jeder Bildvorschau. #}
|
eine Zeile Formular über jeder Bildvorschau. Und nur, wer Formulare
|
||||||
|
ablegen darf, sieht sie überhaupt. #}
|
||||||
|
{% if current_user.has('fileshare_pdf.create') %}
|
||||||
<div class="viewer__save" data-viewer-speichernzeile hidden>
|
<div class="viewer__save" data-viewer-speichernzeile hidden>
|
||||||
<label class="viewer__feld">
|
<label class="viewer__feld">
|
||||||
<span class="field__label">Ordner</span>
|
<span class="field__label">Ordner</span>
|
||||||
@@ -238,6 +253,21 @@
|
|||||||
<span class="field__label">Dateiname (ohne Endung)</span>
|
<span class="field__label">Dateiname (ohne Endung)</span>
|
||||||
<input class="input" data-viewer-name autocomplete="off">
|
<input class="input" data-viewer-name autocomplete="off">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="viewer__feld">
|
||||||
|
<span class="field__label">Speichern als</span>
|
||||||
|
{# Zwei Wege, beide gewollt: weiter ausfüllbar (das Bild wird das
|
||||||
|
Aussehen des Knopfes und lässt sich später austauschen) oder
|
||||||
|
abgeschlossen (alle Felder werden Teil der Seite). #}
|
||||||
|
{# Nur, was das Recht hergibt: "weiter ausfüllbar" hängt an
|
||||||
|
PDF-Formulare/Bearbeiten, "abgeschlossen" an Anlegen. Wer nur
|
||||||
|
anlegen darf, sieht eine Zeile statt einer Auswahl. #}
|
||||||
|
<select class="select" data-viewer-modus>
|
||||||
|
{% if current_user.has('fileshare_pdf.edit') %}
|
||||||
|
<option value="formular">Formular – weiter ausfüllbar</option>
|
||||||
|
{% endif %}
|
||||||
|
<option value="fertig">Abgeschlossen – Felder gesperrt</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input type="checkbox" data-viewer-ersetzen>
|
<input type="checkbox" data-viewer-ersetzen>
|
||||||
<span>Vorhandene ersetzen</span>
|
<span>Vorhandene ersetzen</span>
|
||||||
@@ -245,6 +275,7 @@
|
|||||||
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>Speichern</button>
|
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>Speichern</button>
|
||||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>Abbrechen</button>
|
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="viewer__bar" data-viewer-leiste hidden>
|
<div class="viewer__bar" data-viewer-leiste hidden>
|
||||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
||||||
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
||||||
@@ -256,10 +287,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__footer">
|
<div class="modal__footer">
|
||||||
<a class="btn btn--sm" data-fill-target="Download" data-fill-as="href" href="#" download>
|
{% if current_user.has('fileshare.export') %}
|
||||||
{{ icon('download', size=15) }} Herunterladen
|
<a class="btn btn--sm" data-fill-target="Download" data-fill-as="href" href="#" download>
|
||||||
</a>
|
{{ icon('download', size=15) }} Herunterladen
|
||||||
{% if current_user.has('fileshare.create') %}
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.has('fileshare_pdf.create') %}
|
||||||
<button class="btn btn--sm" type="button" data-viewer-speichern hidden
|
<button class="btn btn--sm" type="button" data-viewer-speichern hidden
|
||||||
data-url="{{ url_for('fileshare.save_form', token=token) }}">
|
data-url="{{ url_for('fileshare.save_form', token=token) }}">
|
||||||
{{ icon('save', size=15) }} Ausgefüllt speichern
|
{{ icon('save', size=15) }} Ausgefüllt speichern
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.3.2"
|
version = "2.4.0"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def create_base_app(
|
|||||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||||
#: Anwendungen.
|
#: Anwendungen.
|
||||||
auth_tagline: str = "",
|
auth_tagline: str = "",
|
||||||
app_version: str = "2.3.2",
|
app_version: str = "2.4.0",
|
||||||
built_in_trusted_keys: tuple[str, ...] = (),
|
built_in_trusted_keys: tuple[str, ...] = (),
|
||||||
enable_license: bool = True,
|
enable_license: bool = True,
|
||||||
template_folder: str | None = None,
|
template_folder: str | None = None,
|
||||||
|
|||||||
@@ -215,44 +215,59 @@ async function pdflibLaden(adresse) {
|
|||||||
return pdflibGeladen;
|
return pdflibGeladen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stempelt die gewaehlten Fotos in die Bildfelder.
|
/** Setzt die Fotos ein und schliesst das Formular auf Wunsch ab.
|
||||||
*
|
*
|
||||||
* Eingabe sind die Bytes, die pdf.js nach dem Ausfuellen geschrieben hat --
|
* Eingabe sind die Bytes, die pdf.js nach dem Ausfuellen geschrieben hat --
|
||||||
* die Formularwerte bleiben also erhalten.
|
* die Feldwerte bleiben also erhalten.
|
||||||
|
*
|
||||||
|
* Das Foto wird das **Aussehen des Bildknopfes** und nicht in die Seite
|
||||||
|
* gestempelt: so bleibt der Knopf erhalten, das Bild ist trotzdem zu sehen,
|
||||||
|
* und beim naechsten Oeffnen laesst es sich austauschen. Genau das tut
|
||||||
|
* Acrobat mit "buttonImportIcon".
|
||||||
|
*
|
||||||
|
* ``abschliessen`` rechnet danach alle Felder flach in die Seite: heraus kommt
|
||||||
|
* ein PDF ohne Formular -- nichts mehr zu verstellen.
|
||||||
*/
|
*/
|
||||||
async function fotosEinsetzen(bytes, bilder, adresse) {
|
async function nachbearbeiten(bytes, bilder, adresse, { abschliessen = false } = {}) {
|
||||||
const PDFLib = await pdflibLaden(adresse);
|
const PDFLib = await pdflibLaden(adresse);
|
||||||
const dokument = await PDFLib.PDFDocument.load(bytes);
|
const dokument = await PDFLib.PDFDocument.load(bytes);
|
||||||
const formular = dokument.getForm();
|
const formular = dokument.getForm();
|
||||||
|
|
||||||
for (const eintrag of bilder) {
|
for (const eintrag of bilder) {
|
||||||
const seite = dokument.getPage(eintrag.seite - 1);
|
|
||||||
const jpeg = await dokument.embedJpg(eintrag.foto.bytes);
|
const jpeg = await dokument.embedJpg(eintrag.foto.bytes);
|
||||||
|
const knopf = eintrag.feldname ? formular.getFieldMaybe(eintrag.feldname) : null;
|
||||||
|
if (knopf && typeof knopf.setImage === "function") {
|
||||||
|
knopf.setImage(jpeg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Kein Knopf gefunden: dann eben in die Seite. Damit nichts darueber
|
||||||
|
// liegt, muss das Feld weichen.
|
||||||
|
const seite = dokument.getPage(eintrag.seite - 1);
|
||||||
const [x1, y1, x2, y2] = eintrag.rechteck;
|
const [x1, y1, x2, y2] = eintrag.rechteck;
|
||||||
const kastenBreite = Math.abs(x2 - x1);
|
const kastenBreite = Math.abs(x2 - x1);
|
||||||
const kastenHoehe = Math.abs(y2 - y1);
|
const kastenHoehe = Math.abs(y2 - y1);
|
||||||
// Einpassen statt verzerren: das Seitenverhaeltnis des Fotos bleibt.
|
// Einpassen statt verzerren: das Seitenverhaeltnis des Fotos bleibt.
|
||||||
const faktor = Math.min(kastenBreite / jpeg.width, kastenHoehe / jpeg.height);
|
const faktor = Math.min(kastenBreite / jpeg.width, kastenHoehe / jpeg.height);
|
||||||
const breite = jpeg.width * faktor;
|
|
||||||
const hoehe = jpeg.height * faktor;
|
|
||||||
seite.drawImage(jpeg, {
|
seite.drawImage(jpeg, {
|
||||||
x: Math.min(x1, x2) + (kastenBreite - breite) / 2,
|
x: Math.min(x1, x2) + (kastenBreite - jpeg.width * faktor) / 2,
|
||||||
y: Math.min(y1, y2) + (kastenHoehe - hoehe) / 2,
|
y: Math.min(y1, y2) + (kastenHoehe - jpeg.height * faktor) / 2,
|
||||||
width: breite,
|
width: jpeg.width * faktor,
|
||||||
height: hoehe,
|
height: jpeg.height * faktor,
|
||||||
});
|
});
|
||||||
// Den Knopf aus der fertigen Kopie nehmen: seine Flaeche liegt ueber dem
|
if (knopf) {
|
||||||
// Seiteninhalt und wuerde das Foto verdecken. Zu tun haette er dort auch
|
try {
|
||||||
// nichts mehr -- seine Aktion fuehrt kein Browser aus, und das Bild steht
|
formular.removeField(knopf);
|
||||||
// jetzt in der Seite. Die Vorlage bleibt unberuehrt.
|
} catch (fehler) {
|
||||||
try {
|
console.warn("Bildknopf liess sich nicht entfernen", fehler);
|
||||||
const feld = formular.getFieldMaybe(eintrag.feldname);
|
}
|
||||||
if (feld) formular.removeField(feld);
|
|
||||||
} catch (fehler) {
|
|
||||||
// Bleibt der Knopf liegen, ist das Foto verdeckt -- aber die Datei ist
|
|
||||||
// heil. Das ist der bessere von zwei schlechten Ausgaengen.
|
|
||||||
console.warn("Bildknopf liess sich nicht entfernen", fehler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abschliessen) {
|
||||||
|
// Alle Felder werden Teil der Seite. Danach gibt es kein Formular mehr --
|
||||||
|
// gewollt: so kann niemand mehr etwas verstellen.
|
||||||
|
formular.flatten();
|
||||||
|
}
|
||||||
return dokument.save();
|
return dokument.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,8 +319,13 @@ async function pdfjsLaden(basis) {
|
|||||||
return pdfjsGeladen;
|
return pdfjsGeladen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
|
/** Zeigt ein PDF -- mit ausfuellbaren Feldern, sofern das erlaubt ist.
|
||||||
async function pdfBetrachter(behaelter, seiten, url, basis) {
|
*
|
||||||
|
* Ohne ``formulare`` bleibt es bei der Darstellung aus dem Dokument: pdf.js
|
||||||
|
* malt dann die Marken, die im PDF stehen (etwa rotes Kreuz und gruener
|
||||||
|
* Haken), statt Bedienelemente des Browsers einzusetzen.
|
||||||
|
*/
|
||||||
|
async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true } = {}) {
|
||||||
const { lib, komponenten } = await pdfjsLaden(basis);
|
const { lib, komponenten } = await pdfjsLaden(basis);
|
||||||
const adressen = pdfjsAdressen(basis);
|
const adressen = pdfjsAdressen(basis);
|
||||||
const eventBus = new komponenten.EventBus();
|
const eventBus = new komponenten.EventBus();
|
||||||
@@ -315,8 +335,11 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
|||||||
viewer: seiten,
|
viewer: seiten,
|
||||||
eventBus,
|
eventBus,
|
||||||
linkService,
|
linkService,
|
||||||
// Der Kern der Sache: Formularfelder werden zu echten Eingabefeldern.
|
// Der Kern der Sache: Formularfelder werden zu echten Eingabefeldern --
|
||||||
annotationMode: lib.AnnotationMode.ENABLE_FORMS,
|
// wer nicht ausfuellen darf, bekommt die Darstellung aus dem Dokument.
|
||||||
|
annotationMode: formulare
|
||||||
|
? lib.AnnotationMode.ENABLE_FORMS
|
||||||
|
: lib.AnnotationMode.ENABLE,
|
||||||
textLayerMode: 1,
|
textLayerMode: 1,
|
||||||
});
|
});
|
||||||
linkService.setViewer(viewer);
|
linkService.setViewer(viewer);
|
||||||
@@ -364,6 +387,7 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
|||||||
const bilder = new Map();
|
const bilder = new Map();
|
||||||
|
|
||||||
const bildknoepfeAufsetzen = async (nurSeite) => {
|
const bildknoepfeAufsetzen = async (nurSeite) => {
|
||||||
|
if (!formulare) return;
|
||||||
for (let nummer = 1; nummer <= dokument.numPages; nummer++) {
|
for (let nummer = 1; nummer <= dokument.numPages; nummer++) {
|
||||||
if (nurSeite && nummer !== nurSeite) continue;
|
if (nurSeite && nummer !== nurSeite) continue;
|
||||||
const seite = await dokument.getPage(nummer);
|
const seite = await dokument.getPage(nummer);
|
||||||
@@ -439,10 +463,11 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
|||||||
* pdf.js schreibt die Feldwerte; gewaehlte Fotos kommen danach mit
|
* pdf.js schreibt die Feldwerte; gewaehlte Fotos kommen danach mit
|
||||||
* pdf-lib hinein -- pdf.js kann nichts in ein PDF zeichnen.
|
* pdf-lib hinein -- pdf.js kann nichts in ein PDF zeichnen.
|
||||||
*/
|
*/
|
||||||
speichern: async (pdflibAdresse) => {
|
speichern: async (pdflibAdresse, { abschliessen = false } = {}) => {
|
||||||
const bytes = await dokument.saveDocument();
|
const bytes = await dokument.saveDocument();
|
||||||
if (!bilder.size) return bytes;
|
// Ohne Bild und ohne Abschluss braucht es pdf-lib gar nicht.
|
||||||
return fotosEinsetzen(bytes, [...bilder.values()], pdflibAdresse);
|
if (!bilder.size && !abschliessen) return bytes;
|
||||||
|
return nachbearbeiten(bytes, [...bilder.values()], pdflibAdresse, { abschliessen });
|
||||||
},
|
},
|
||||||
/** Wurde ueberhaupt etwas eingetragen? */
|
/** Wurde ueberhaupt etwas eingetragen? */
|
||||||
veraendert: () => {
|
veraendert: () => {
|
||||||
@@ -482,6 +507,7 @@ export function initFilePreview(wurzel = document) {
|
|||||||
const ersetzenFeld = stueck("ersetzen");
|
const ersetzenFeld = stueck("ersetzen");
|
||||||
const jetzt = stueck("jetzt");
|
const jetzt = stueck("jetzt");
|
||||||
const abbrechen = stueck("abbrechen");
|
const abbrechen = stueck("abbrechen");
|
||||||
|
const modusFeld = stueck("modus");
|
||||||
const basis = rumpf.dataset.viewerPdfjs || "";
|
const basis = rumpf.dataset.viewerPdfjs || "";
|
||||||
|
|
||||||
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
||||||
@@ -545,7 +571,9 @@ export function initFilePreview(wurzel = document) {
|
|||||||
}
|
}
|
||||||
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
||||||
try {
|
try {
|
||||||
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis);
|
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis, {
|
||||||
|
formulare: rumpf.dataset.viewerFormulare === "1",
|
||||||
|
});
|
||||||
if (hinweis) hinweis.textContent = "";
|
if (hinweis) hinweis.textContent = "";
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
||||||
@@ -619,7 +647,9 @@ export function initFilePreview(wurzel = document) {
|
|||||||
const vorher = jetzt.textContent;
|
const vorher = jetzt.textContent;
|
||||||
jetzt.textContent = "Speichern ...";
|
jetzt.textContent = "Speichern ...";
|
||||||
try {
|
try {
|
||||||
const bytes = await pdf.speichern(rumpf.dataset.viewerPdflib || "");
|
const bytes = await pdf.speichern(rumpf.dataset.viewerPdflib || "", {
|
||||||
|
abschliessen: modusFeld?.value === "fertig",
|
||||||
|
});
|
||||||
const daten = new FormData();
|
const daten = new FormData();
|
||||||
daten.append("pfad", aktuell.pfad);
|
daten.append("pfad", aktuell.pfad);
|
||||||
daten.append("ordner", ordnerFeld.value.trim());
|
daten.append("ordner", ordnerFeld.value.trim());
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.3.2"
|
version = "2.4.0"
|
||||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["cryptography>=42"]
|
dependencies = ["cryptography>=42"]
|
||||||
|
|||||||
Reference in new issue
Block a user