Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2679361bc | ||
|
|
20c7fd8813 | ||
|
|
eaca6f7990 | ||
|
|
4eb1c0c2d8 |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -86,7 +86,10 @@ def browse():
|
||||
|
||||
|
||||
@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):
|
||||
mount = _mount_or_404(token)
|
||||
relative = request.args.get("pfad") or ""
|
||||
@@ -184,7 +187,7 @@ def upload(token: str):
|
||||
|
||||
|
||||
@bp.post("/<token>/formular")
|
||||
@permission_required("fileshare.create")
|
||||
@permission_required("fileshare_pdf.create")
|
||||
def save_form(token: str):
|
||||
"""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.
|
||||
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.
|
||||
ordner = (request.form.get("ordner") or "").strip().strip("/")
|
||||
wunschname = (request.form.get("name") or "").strip()
|
||||
@@ -214,10 +228,19 @@ def save_form(token: str):
|
||||
|
||||
try:
|
||||
original = service.resolve_path(mount, relative)
|
||||
zielordner = service.resolve_path(mount, ordner) if ordner else original.parent
|
||||
# Mit Namen entscheidet das Ordnerfeld -- leer heisst Wurzelverzeichnis,
|
||||
# so wie es im Feld steht. Ohne Namen (Aufruf ohne Oberflaeche) bleibt
|
||||
# es beim Ordner des Originals.
|
||||
if wunschname:
|
||||
zielordner = service.resolve_path(mount, ordner)
|
||||
else:
|
||||
zielordner = service.resolve_path(mount, ordner) if ordner else original.parent
|
||||
if not zielordner.is_dir():
|
||||
return {"error": f"Den Ordner '{ordner}' gibt es in dieser Freigabe nicht."}, 400
|
||||
if wunschname:
|
||||
# Eingegeben wird der Name ohne Endung; die kommt von hier. Wer
|
||||
# sie mittippt, bekommt sie nicht doppelt -- und wer eine falsche
|
||||
# tippt, bekommt trotzdem eine Datei, die sich oeffnen laesst.
|
||||
name = service.safe_filename(wunschname)
|
||||
if not name.lower().endswith(".pdf"):
|
||||
name += ".pdf"
|
||||
|
||||
@@ -85,11 +85,30 @@ DHCP = Resource(
|
||||
FILESHARE = Resource(
|
||||
key="fileshare",
|
||||
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)",
|
||||
action_hints={
|
||||
A.CREATE: "Ordner anlegen und Dateien hochladen.",
|
||||
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",
|
||||
label="Dateifreigaben",
|
||||
icon="folder",
|
||||
resources=(FILESHARE,),
|
||||
resources=(FILESHARE, FILESHARE_PDF),
|
||||
)
|
||||
|
||||
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, ...] = (
|
||||
Migration(version=1, name="baseline", sql=V1),
|
||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||
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
|
||||
|
||||
import zlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
@@ -548,6 +549,10 @@ def list_directory(mount: Mount, relative: str = "") -> dict[str, Any]:
|
||||
else:
|
||||
record["previewable"] = entry.suffix.lower() in PREVIEW_MIME
|
||||
record["preview_kind"] = preview_kind(entry.name)
|
||||
# Getrennt, damit die Oberflaeche nur den Namen zum Bearbeiten
|
||||
# gibt und die Endung danebenstellt.
|
||||
record["stem"] = entry.stem
|
||||
record["suffix"] = entry.suffix
|
||||
files.append(record)
|
||||
except OSError as exc:
|
||||
raise FileshareError(f"Verzeichnis nicht lesbar ({exc}).") from exc
|
||||
@@ -618,6 +623,29 @@ def all_directories(mount: Mount, *, tiefe: int = 4, grenze: int = 400) -> list[
|
||||
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:
|
||||
"""Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``.
|
||||
|
||||
@@ -656,8 +684,26 @@ def make_directory(mount: Mount, relative: str, name: str) -> Path:
|
||||
|
||||
|
||||
def rename_entry(mount: Mount, relative: str, new_name: str) -> Path:
|
||||
"""Benennt um und behaelt die Endung der Datei.
|
||||
|
||||
Eingegeben wird nur der Name ohne Endung. Eine Datei, die danach auf
|
||||
``.pdf.txt`` oder auf gar nichts endet, waere auf einer Windows-Freigabe
|
||||
praktisch kaputt -- sie oeffnet sich nicht mehr mit dem richtigen
|
||||
Programm. Verzeichnisse haben keine Endung; dort bleibt der Name, wie er
|
||||
eingegeben wurde.
|
||||
|
||||
Wer die Endung trotzdem mittippt, bekommt sie nicht doppelt.
|
||||
"""
|
||||
path = resolve_path(mount, relative)
|
||||
target = path.parent / safe_filename(new_name)
|
||||
endung = path.suffix if path.is_file() else ""
|
||||
gewuenscht = (new_name or "").strip()
|
||||
if endung and gewuenscht.lower().endswith(endung.lower()):
|
||||
gewuenscht = gewuenscht[: -len(endung)]
|
||||
if not gewuenscht:
|
||||
raise FileshareError("Der Name darf nicht leer sein.")
|
||||
target = path.parent / safe_filename(gewuenscht + endung)
|
||||
if target == path:
|
||||
return path
|
||||
if target.exists():
|
||||
raise FileshareError("Ein Eintrag mit diesem Namen existiert bereits.")
|
||||
path.rename(target)
|
||||
|
||||
@@ -106,14 +106,22 @@
|
||||
data-fill-Kind="{{ entry.preview_kind }}"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
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 }}
|
||||
</button>
|
||||
{% else %}
|
||||
<a class="link-icon"
|
||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}">
|
||||
{{ icon('file-text', size=15) }} {{ entry.name }}
|
||||
</a>
|
||||
{% if current_user.has('fileshare.export') %}
|
||||
<a class="link-icon"
|
||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}">
|
||||
{{ 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 %}
|
||||
</td>
|
||||
<td class="numeric fs-sm">{{ entry.size | bytes }}</td>
|
||||
@@ -126,19 +134,22 @@
|
||||
data-fill-Kind="{{ entry.preview_kind }}"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
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>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare.edit') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="file-rename"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
data-fill-Name="{{ entry.name }}"
|
||||
data-fill-Name="{{ entry.stem }}"
|
||||
data-fill-Endung="{{ entry.suffix or '' }}"
|
||||
aria-label="Umbenennen">{{ icon('pencil', size=15) }}</button>
|
||||
{% endif %}
|
||||
<a class="btn btn--sm btn--ghost"
|
||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"
|
||||
aria-label="Herunterladen">{{ icon('download', size=15) }}</a>
|
||||
{% if current_user.has('fileshare.export') %}
|
||||
<a class="btn btn--sm btn--ghost"
|
||||
href="{{ url_for('fileshare.download', token=token, pfad=entry.relative) }}"
|
||||
aria-label="Herunterladen">{{ icon('download', size=15) }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -209,7 +220,11 @@
|
||||
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
||||
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
||||
<div class="modal__body modal__body--flush" data-viewer
|
||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}">
|
||||
{# 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-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}">
|
||||
<div class="viewer">
|
||||
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
||||
<img class="viewer__img" alt="" data-viewer-bild>
|
||||
@@ -225,17 +240,34 @@
|
||||
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
||||
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
||||
{# 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>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Ordner</span>
|
||||
<input class="input" list="freigabe-ordner" data-viewer-ordner
|
||||
placeholder="Wurzelverzeichnis" autocomplete="off">
|
||||
placeholder="Wurzelverzeichnis der Freigabe" autocomplete="off">
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Dateiname</span>
|
||||
<span class="field__label">Dateiname (ohne Endung)</span>
|
||||
<input class="input" data-viewer-name autocomplete="off">
|
||||
</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">
|
||||
<input type="checkbox" data-viewer-ersetzen>
|
||||
<span>Vorhandene ersetzen</span>
|
||||
@@ -243,6 +275,7 @@
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="viewer__bar" data-viewer-leiste hidden>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
||||
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
||||
@@ -254,10 +287,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__footer">
|
||||
<a class="btn btn--sm" data-fill-target="Download" data-fill-as="href" href="#" download>
|
||||
{{ icon('download', size=15) }} Herunterladen
|
||||
</a>
|
||||
{% if current_user.has('fileshare.create') %}
|
||||
{% if current_user.has('fileshare.export') %}
|
||||
<a class="btn btn--sm" data-fill-target="Download" data-fill-as="href" href="#" download>
|
||||
{{ icon('download', size=15) }} Herunterladen
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare_pdf.create') %}
|
||||
<button class="btn btn--sm" type="button" data-viewer-speichern hidden
|
||||
data-url="{{ url_for('fileshare.save_form', token=token) }}">
|
||||
{{ icon('save', size=15) }} Ausgefüllt speichern
|
||||
@@ -279,9 +314,12 @@
|
||||
<input type="hidden" name="parent" value="{{ listing.relative }}">
|
||||
<input type="hidden" name="pfad" data-fill-target="Pfad" value="">
|
||||
<div class="modal__body">
|
||||
{{ field('name', 'Neuer Name', required=True, attrs='data-fill-target="Name"') }}
|
||||
{{ field('name', 'Neuer Name (ohne Endung)', required=True,
|
||||
attrs='data-fill-target="Name"') }}
|
||||
<p class="fs-xs faint">
|
||||
Umbenannt wird auf der Freigabe selbst -- es gibt dort keinen Papierkorb.
|
||||
Die Endung <code data-fill-target="Endung"></code> bleibt, wie sie ist --
|
||||
eine Datei ohne passende Endung öffnet auf der Freigabe nichts mehr.
|
||||
Umbenannt wird dort direkt; einen Papierkorb gibt es nicht.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal__footer">
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -135,7 +135,7 @@ def create_base_app(
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.3.0",
|
||||
app_version: str = "2.4.0",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -720,6 +720,16 @@
|
||||
min-height: 220px;
|
||||
background: var(--surface-1);
|
||||
}
|
||||
/* Hochkant -- Tablet oder Monitor im Hochformat -- gibt es unter dem Dialog
|
||||
viel Platz, den die Obergrenze verschenkt. Beim Ausfuellen eines
|
||||
Formulars zaehlt jede sichtbare Zeile, also nehmen wir dort die Hoehe,
|
||||
die da ist. Der Rest (Kopf, Dateiname, Fusszeile) braucht rund 200 Pixel;
|
||||
die 240 lassen etwas Luft. */
|
||||
@media (orientation: portrait) {
|
||||
.viewer {
|
||||
height: calc(100dvh - 240px);
|
||||
}
|
||||
}
|
||||
.viewer > .file-preview {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
@@ -763,6 +773,19 @@
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
/* Ein PDF ist weisses Papier. pdf.js malt Formularfelder mit Systemfarben
|
||||
und ueberlaesst Ankreuzfelder dem Browser -- im dunklen Design heisst
|
||||
CanvasText weiss, und die Haken waren auf dem Papier unsichtbar. Fuer
|
||||
diesen Ausschnitt gilt deshalb immer das helle Schema. */
|
||||
color-scheme: light;
|
||||
}
|
||||
/* pdf.js rechnet fuer die Seite mit dem Standard "content-box": ihr
|
||||
9-Pixel-Rahmen liegt *ausserhalb* der angegebenen Breite. Unser globales
|
||||
"border-box" rechnete ihn hinein -- die Leinwand wurde 18 Pixel schmaler
|
||||
als die Anmerkungsebene, und die Formularfelder wanderten nach rechts
|
||||
unten, immer weiter je weiter unten sie lagen. */
|
||||
.viewer__pdf .pdfViewer .page {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.viewer__pdfscroll {
|
||||
position: absolute;
|
||||
@@ -781,6 +804,36 @@
|
||||
.viewer__hint {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
/* Bildfeld eines Formulars: der Kasten zeigt das gewaehlte Foto, die
|
||||
Schaltflaeche liegt darin. Acrobats eigener Knopf tut im Browser nichts --
|
||||
das hier ist der Ersatz. */
|
||||
.viewer__bildfeld [data-viewer-bildvorschau] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
.viewer__bildwahl {
|
||||
position: absolute;
|
||||
inset: auto 0 4px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.viewer__bildwahl > span {
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--r-sm, 6px);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-sm, 0 1px 3px rgb(0 0 0 / 0.3));
|
||||
}
|
||||
.viewer__bildfeld--belegt .viewer__bildwahl > span {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.viewer__save {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -142,6 +142,135 @@ function bildBetrachter(buehne, bild) {
|
||||
};
|
||||
}
|
||||
|
||||
/* -- Bildfelder in Formularen ---------------------------------------------- */
|
||||
|
||||
//: Groesser muss ein Foto in einem Formular nicht sein.
|
||||
const FOTO_KANTE = 1600;
|
||||
|
||||
/** Macht aus einer gewaehlten Datei ein JPEG in vertretbarer Groesse.
|
||||
*
|
||||
* Ueber eine Leinwand, nicht roh: damit wird aus allem, was der Browser
|
||||
* anzeigen kann, ein JPEG -- auf einem iPad also auch aus HEIC, das pdf-lib
|
||||
* nicht kennt. Und ein 12-Megapixel-Foto blaeht das Formular nicht auf.
|
||||
*/
|
||||
async function fotoAlsJpeg(datei) {
|
||||
// Keine Objektadresse: die CSP erlaubt Bilder nur von "self" und als data:.
|
||||
// createImageBitmap nimmt die Datei unmittelbar; wo es fehlt, tut es eine
|
||||
// data:-Adresse.
|
||||
let bild;
|
||||
if (typeof createImageBitmap === "function") {
|
||||
try {
|
||||
bild = await createImageBitmap(datei);
|
||||
} catch {
|
||||
bild = null;
|
||||
}
|
||||
}
|
||||
if (!bild) {
|
||||
const daten = await new Promise((fertig, schiefgegangen) => {
|
||||
const leser = new FileReader();
|
||||
leser.onload = () => fertig(String(leser.result));
|
||||
leser.onerror = () => schiefgegangen(new Error("Die Datei liess sich nicht lesen."));
|
||||
leser.readAsDataURL(datei);
|
||||
});
|
||||
bild = await new Promise((fertig, schiefgegangen) => {
|
||||
const el = new Image();
|
||||
el.onload = () => fertig(el);
|
||||
el.onerror = () => schiefgegangen(new Error("Dieses Bild kann der Browser nicht anzeigen."));
|
||||
el.src = daten;
|
||||
});
|
||||
}
|
||||
const breiteRoh = bild.width || bild.naturalWidth;
|
||||
const hoeheRoh = bild.height || bild.naturalHeight;
|
||||
const faktor = Math.min(1, FOTO_KANTE / Math.max(breiteRoh, hoeheRoh));
|
||||
const leinwand = document.createElement("canvas");
|
||||
leinwand.width = Math.max(1, Math.round(breiteRoh * faktor));
|
||||
leinwand.height = Math.max(1, Math.round(hoeheRoh * faktor));
|
||||
leinwand.getContext("2d").drawImage(bild, 0, 0, leinwand.width, leinwand.height);
|
||||
bild.close?.();
|
||||
const klecks = await new Promise((fertig) => leinwand.toBlob(fertig, "image/jpeg", 0.85));
|
||||
return {
|
||||
bytes: new Uint8Array(await klecks.arrayBuffer()),
|
||||
breite: leinwand.width,
|
||||
hoehe: leinwand.height,
|
||||
vorschau: leinwand.toDataURL("image/jpeg", 0.6),
|
||||
};
|
||||
}
|
||||
|
||||
let pdflibGeladen = null;
|
||||
|
||||
/** pdf-lib nachladen -- nur wenn wirklich ein Bild eingesetzt wird. */
|
||||
async function pdflibLaden(adresse) {
|
||||
if (!pdflibGeladen) {
|
||||
pdflibGeladen = new Promise((fertig, schiefgegangen) => {
|
||||
const skript = document.createElement("script");
|
||||
skript.src = adresse;
|
||||
skript.onload = () => fertig(window.PDFLib);
|
||||
skript.onerror = () => {
|
||||
pdflibGeladen = null;
|
||||
schiefgegangen(new Error("pdf-lib liess sich nicht laden."));
|
||||
};
|
||||
document.head.append(skript);
|
||||
});
|
||||
}
|
||||
return pdflibGeladen;
|
||||
}
|
||||
|
||||
/** Setzt die Fotos ein und schliesst das Formular auf Wunsch ab.
|
||||
*
|
||||
* Eingabe sind die Bytes, die pdf.js nach dem Ausfuellen geschrieben hat --
|
||||
* 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 nachbearbeiten(bytes, bilder, adresse, { abschliessen = false } = {}) {
|
||||
const PDFLib = await pdflibLaden(adresse);
|
||||
const dokument = await PDFLib.PDFDocument.load(bytes);
|
||||
const formular = dokument.getForm();
|
||||
|
||||
for (const eintrag of bilder) {
|
||||
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 kastenBreite = Math.abs(x2 - x1);
|
||||
const kastenHoehe = Math.abs(y2 - y1);
|
||||
// Einpassen statt verzerren: das Seitenverhaeltnis des Fotos bleibt.
|
||||
const faktor = Math.min(kastenBreite / jpeg.width, kastenHoehe / jpeg.height);
|
||||
seite.drawImage(jpeg, {
|
||||
x: Math.min(x1, x2) + (kastenBreite - jpeg.width * faktor) / 2,
|
||||
y: Math.min(y1, y2) + (kastenHoehe - jpeg.height * faktor) / 2,
|
||||
width: jpeg.width * faktor,
|
||||
height: jpeg.height * faktor,
|
||||
});
|
||||
if (knopf) {
|
||||
try {
|
||||
formular.removeField(knopf);
|
||||
} catch (fehler) {
|
||||
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();
|
||||
}
|
||||
|
||||
/* -- PDF ------------------------------------------------------------------- */
|
||||
|
||||
let pdfjsGeladen = null;
|
||||
@@ -190,8 +319,13 @@ async function pdfjsLaden(basis) {
|
||||
return pdfjsGeladen;
|
||||
}
|
||||
|
||||
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
|
||||
async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
/** Zeigt ein PDF -- mit ausfuellbaren Feldern, sofern das erlaubt ist.
|
||||
*
|
||||
* 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 adressen = pdfjsAdressen(basis);
|
||||
const eventBus = new komponenten.EventBus();
|
||||
@@ -201,8 +335,11 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
viewer: seiten,
|
||||
eventBus,
|
||||
linkService,
|
||||
// Der Kern der Sache: Formularfelder werden zu echten Eingabefeldern.
|
||||
annotationMode: lib.AnnotationMode.ENABLE_FORMS,
|
||||
// Der Kern der Sache: Formularfelder werden zu echten Eingabefeldern --
|
||||
// wer nicht ausfuellen darf, bekommt die Darstellung aus dem Dokument.
|
||||
annotationMode: formulare
|
||||
? lib.AnnotationMode.ENABLE_FORMS
|
||||
: lib.AnnotationMode.ENABLE,
|
||||
textLayerMode: 1,
|
||||
});
|
||||
linkService.setViewer(viewer);
|
||||
@@ -244,6 +381,73 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
start = null;
|
||||
});
|
||||
|
||||
/* Bildknoepfe: eigene Schaltflaeche darueber, denn Acrobats Aktion
|
||||
("buttonImportIcon") fuehrt kein Browser aus. Gesammelt wird, was beim
|
||||
Speichern in die Datei gestempelt werden soll. */
|
||||
const bilder = new Map();
|
||||
|
||||
const bildknoepfeAufsetzen = async (nurSeite) => {
|
||||
if (!formulare) return;
|
||||
for (let nummer = 1; nummer <= dokument.numPages; nummer++) {
|
||||
if (nurSeite && nummer !== nurSeite) continue;
|
||||
const seite = await dokument.getPage(nummer);
|
||||
const annots = await seite.getAnnotations({ intent: "display" });
|
||||
for (const annot of annots) {
|
||||
const quelltext = (annot.actions?.Action || []).join(" ");
|
||||
if (!quelltext.includes("buttonImportIcon")) continue;
|
||||
const kasten = behaelter.querySelector(
|
||||
`section[data-annotation-id="${annot.id}"]`
|
||||
);
|
||||
if (!kasten || kasten.querySelector("[data-viewer-bildwahl]")) continue;
|
||||
kasten.classList.add("viewer__bildfeld");
|
||||
const knopf = document.createElement("label");
|
||||
knopf.className = "viewer__bildwahl";
|
||||
knopf.setAttribute("data-viewer-bildwahl", "");
|
||||
knopf.innerHTML =
|
||||
'<input type="file" accept="image/*" hidden>' +
|
||||
"<span>Bild einsetzen</span>";
|
||||
const feld = knopf.querySelector("input");
|
||||
feld.addEventListener("change", async () => {
|
||||
const datei = feld.files?.[0];
|
||||
if (!datei) return;
|
||||
try {
|
||||
const foto = await fotoAlsJpeg(datei);
|
||||
bilder.set(annot.id, {
|
||||
seite: nummer,
|
||||
rechteck: annot.rect,
|
||||
feldname: annot.fieldName,
|
||||
foto,
|
||||
});
|
||||
// Als eigenes Bild und nicht als Hintergrund: der Kasten traegt
|
||||
// eine Leinwand von pdf.js (das Aussehen des Knopfes aus dem PDF),
|
||||
// und die liegt ueber jedem Hintergrund.
|
||||
let vorschau = kasten.querySelector("[data-viewer-bildvorschau]");
|
||||
if (!vorschau) {
|
||||
vorschau = document.createElement("img");
|
||||
vorschau.setAttribute("data-viewer-bildvorschau", "");
|
||||
vorschau.alt = "";
|
||||
kasten.append(vorschau);
|
||||
}
|
||||
vorschau.src = foto.vorschau;
|
||||
kasten.classList.add("viewer__bildfeld--belegt");
|
||||
knopf.querySelector("span").textContent = "Anderes Bild";
|
||||
} catch (fehler) {
|
||||
window.tesm.showToast(fehler.message || "Bild nicht lesbar.", "danger");
|
||||
}
|
||||
});
|
||||
kasten.append(knopf);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Die Knoepfe koennen erst auf eine gerenderte Anmerkungsebene -- die
|
||||
// entsteht je Seite, wenn sie sichtbar wird.
|
||||
eventBus.on("annotationlayerrendered", (ereignis) => {
|
||||
bildknoepfeAufsetzen(ereignis.pageNumber).catch((fehler) =>
|
||||
console.error("Bildfelder", fehler)
|
||||
);
|
||||
});
|
||||
await bildknoepfeAufsetzen();
|
||||
|
||||
return {
|
||||
hinein: () => {
|
||||
viewer.currentScale = Math.min(MAX_ZOOM, viewer.currentScale * 1.2);
|
||||
@@ -254,8 +458,17 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
anpassen: () => {
|
||||
viewer.currentScaleValue = "page-width";
|
||||
},
|
||||
/** Das ausgefuellte Formular als Bytes -- pdf.js schreibt die Werte hinein. */
|
||||
speichern: () => dokument.saveDocument(),
|
||||
/** Das ausgefuellte Formular als Bytes.
|
||||
*
|
||||
* pdf.js schreibt die Feldwerte; gewaehlte Fotos kommen danach mit
|
||||
* pdf-lib hinein -- pdf.js kann nichts in ein PDF zeichnen.
|
||||
*/
|
||||
speichern: async (pdflibAdresse, { abschliessen = false } = {}) => {
|
||||
const bytes = await dokument.saveDocument();
|
||||
// Ohne Bild und ohne Abschluss braucht es pdf-lib gar nicht.
|
||||
if (!bilder.size && !abschliessen) return bytes;
|
||||
return nachbearbeiten(bytes, [...bilder.values()], pdflibAdresse, { abschliessen });
|
||||
},
|
||||
/** Wurde ueberhaupt etwas eingetragen? */
|
||||
veraendert: () => {
|
||||
const s = dokument.annotationStorage;
|
||||
@@ -294,6 +507,7 @@ export function initFilePreview(wurzel = document) {
|
||||
const ersetzenFeld = stueck("ersetzen");
|
||||
const jetzt = stueck("jetzt");
|
||||
const abbrechen = stueck("abbrechen");
|
||||
const modusFeld = stueck("modus");
|
||||
const basis = rumpf.dataset.viewerPdfjs || "";
|
||||
|
||||
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
||||
@@ -357,7 +571,9 @@ export function initFilePreview(wurzel = document) {
|
||||
}
|
||||
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
||||
try {
|
||||
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis);
|
||||
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis, {
|
||||
formulare: rumpf.dataset.viewerFormulare === "1",
|
||||
});
|
||||
if (hinweis) hinweis.textContent = "";
|
||||
} catch (fehler) {
|
||||
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
||||
@@ -405,9 +621,10 @@ export function initFilePreview(wurzel = document) {
|
||||
const teile = aktuell.pfad.split("/");
|
||||
const datei = teile.pop() || "formular.pdf";
|
||||
ordnerFeld.value = teile.join("/");
|
||||
// Der Name der Vorlage mit Zusatz -- die Vorlage selbst bleibt so heil,
|
||||
// auch wenn jemand blind auf Speichern drueckt.
|
||||
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt.pdf";
|
||||
// Ohne Endung: die haengt der Server an. Eine Datei, die auf ".pdf.txt"
|
||||
// oder auf gar nichts endet, oeffnet auf einer Freigabe nichts mehr.
|
||||
// Der Zusatz haelt die Vorlage heil, auch wenn jemand blind speichert.
|
||||
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt";
|
||||
ersetzenFeld.checked = false;
|
||||
};
|
||||
|
||||
@@ -430,7 +647,9 @@ export function initFilePreview(wurzel = document) {
|
||||
const vorher = jetzt.textContent;
|
||||
jetzt.textContent = "Speichern ...";
|
||||
try {
|
||||
const bytes = await pdf.speichern();
|
||||
const bytes = await pdf.speichern(rumpf.dataset.viewerPdflib || "", {
|
||||
abschliessen: modusFeld?.value === "fertig",
|
||||
});
|
||||
const daten = new FormData();
|
||||
daten.append("pfad", aktuell.pfad);
|
||||
daten.append("ordner", ordnerFeld.value.trim());
|
||||
@@ -439,7 +658,7 @@ export function initFilePreview(wurzel = document) {
|
||||
daten.append(
|
||||
"datei",
|
||||
new Blob([bytes], { type: "application/pdf" }),
|
||||
nameFeld.value.trim() || "formular.pdf"
|
||||
`${nameFeld.value.trim() || "formular"}.pdf`
|
||||
);
|
||||
const antwort = await window.tesm.api(ziel, { method: "POST", body: daten });
|
||||
window.tesm.showToast(antwort.message || "Gespeichert.", "success");
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
pdf-lib 1.17.1 -- https://github.com/Hopding/pdf-lib (MIT, siehe LICENSE)
|
||||
|
||||
Aus dem CDN-Bau "pdf-lib.min.js" (UMD, setzt window.PDFLib).
|
||||
|
||||
Wofuer: pdf.js kann Formulare ausfuellen, aber nichts in ein PDF zeichnen. Fuer
|
||||
den Bildknopf in Formularen (Acrobats "buttonImportIcon", das kein Browser
|
||||
ausfuehren kann) wird das gewaehlte Foto damit in das Feld gestempelt.
|
||||
|
||||
Geladen wird die Datei erst, wenn wirklich ein Bild eingesetzt wird.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Andrew Dillon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
1.17.1
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).PDFLib={})}(this,(function(t){"use strict";
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||
|
||||
See the Apache Version 2.0 License for specific language governing permissions
|
||||
and limitations under the License.
|
||||
***************************************************************************** */var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,n)};function n(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}var r=function(){return(r=Object.assign||function(t){for(var e,n=1,r=arguments.length;n<r;n++)for(var i in e=arguments[n])Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t}).apply(this,arguments)};function i(t,e,n,r){return new(n||(n=Promise))((function(i,o){function a(t){try{l(r.next(t))}catch(t){o(t)}}function s(t){try{l(r.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(a,s)}l((r=r.apply(t,e||[])).next())}))}function o(t,e){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=a.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]<i[3])){a.label=o[1];break}if(6===o[0]&&a.label<i[1]){a.label=i[1],i=o;break}if(i&&a.label<i[2]){a.label=i[2],a.ops.push(o);break}i[2]&&a.ops.pop(),a.trys.pop();continue}o=e.call(t,a)}catch(t){o=[6,t],r=0}finally{n=i=0}if(5&o[0])throw o[1];return{value:o[0]?o[1]:void 0,done:!0}}([o,s])}}}function a(){for(var t=0,e=0,n=arguments.length;e<n;e++)t+=arguments[e].length;var r=Array(t),i=0;for(e=0;e<n;e++)for(var o=arguments[e],a=0,s=o.length;a<s;a++,i++)r[i]=o[a];return r}for(var s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",l=new Uint8Array(256),h=0;h<s.length;h++)l[s.charCodeAt(h)]=h;var u,c=function(t){for(var e="",n=t.length,r=0;r<n;r+=3)e+=s[t[r]>>2],e+=s[(3&t[r])<<4|t[r+1]>>4],e+=s[(15&t[r+1])<<2|t[r+2]>>6],e+=s[63&t[r+2]];return n%3==2?e=e.substring(0,e.length-1)+"=":n%3==1&&(e=e.substring(0,e.length-2)+"=="),e},d=function(t){var e,n,r,i,o,a=.75*t.length,s=t.length,h=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new Uint8Array(a);for(e=0;e<s;e+=4)n=l[t.charCodeAt(e)],r=l[t.charCodeAt(e+1)],i=l[t.charCodeAt(e+2)],o=l[t.charCodeAt(e+3)],u[h++]=n<<2|r>>4,u[h++]=(15&r)<<4|i>>2,u[h++]=(3&i)<<6|63&o;return u},f=/^(data)?:?([\w\/\+]+)?;?(charset=[\w-]+|base64)?.*,/i,p=function(t){var e=t.trim(),n=e.substring(0,100).match(f);if(!n)return d(e);var r=n[0],i=e.substring(r.length);return d(i)},g=function(t){return t.charCodeAt(0)},v=function(t){return t.codePointAt(0)},y=function(t,e){return x(t.toString(16),e,"0").toUpperCase()},m=function(t){return y(t,2)},b=function(t){return String.fromCharCode(t)},w=function(t){return b(parseInt(t,16))},x=function(t,e,n){for(var r="",i=0,o=e-t.length;i<o;i++)r+=n;return r+t},k=function(t,e,n){for(var r=t.length,i=0;i<r;i++)e[n++]=t.charCodeAt(i);return r},F=function(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")},S=function(t){return t.replace(/\t|\u0085|\u2028|\u2029/g," ").replace(/[\b\v]/g,"")},C=["\\n","\\f","\\r","\\u000B"],A=function(t){return/^[\n\f\r\u000B]$/.test(t)},T=function(t){return t.split(/[\n\f\r\u000B]/)},P=function(t){return t.replace(/[\n\f\r\u000B]/g," ")},z=function(t,e){var n,r=t.charCodeAt(e),i=e+1,o=1;return r>=55296&&r<=56319&&t.length>i&&(n=t.charCodeAt(i))>=56320&&n<=57343&&(o=2),[t.slice(e,e+o),o]},R=function(t){for(var e=[],n=0,r=t.length;n<r;){var i=z(t,n),o=i[0],a=i[1];e.push(o),n+=a}return e},O=function(t,e,n,r){for(var i=function(t){for(var e=C.join("|"),n=["$"],r=0,i=t.length;r<i;r++){var o=t[r];if(A(o))throw new TypeError("`wordBreak` must not include "+e);n.push(""===o?".":F(o))}var a=n.join("|");return new RegExp("("+e+")|((.*?)("+a+"))","gm")}(e),o=S(t).match(i),a="",s=0,l=[],h=function(){""!==a&&l.push(a),a="",s=0},u=0,c=o.length;u<c;u++){var d=o[u];if(A(d))h();else{var f=r(d);s+f>n&&h(),a+=d,s+=f}}return h(),l},D=/^D:(\d\d\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([+\-Z])?(\d\d)?'?(\d\d)?'?$/,B=function(t){var e=t.match(D);if(e){var n=e[1],r=e[2],i=void 0===r?"01":r,o=e[3],a=void 0===o?"01":o,s=e[4],l=void 0===s?"00":s,h=e[5],u=void 0===h?"00":h,c=e[6],d=void 0===c?"00":c,f=e[7],p=void 0===f?"Z":f,g=e[8],v=void 0===g?"00":g,y=e[9];return new Date(n+"-"+i+"-"+a+"T"+l+":"+u+":"+d+("Z"===p?"Z":""+p+v+":"+(void 0===y?"00":y)))}},N=function(t,e){for(var n,r,i=0;i<t.length;){var o=t.substring(i).match(e);if(!o)return{match:r,pos:i};r=o,i+=(null!==(n=o.index)&&void 0Line truncated
|
||||
//# sourceMappingURL=pdf-lib.min.js.map
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in new issue
Block a user