Compare commits

...
1 Commits
5 changed files with 122 additions and 17 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm" name = "tesm"
version = "2.7.7" version = "2.7.8"
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 = [
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.7.7" version = "2.7.8"
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 = [
@@ -148,7 +148,11 @@ 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.7.7", #: Beide Anwendungen geben ihre eigene Paketversion mit -- und die laeuft
#: absichtlich auseinander: was sich nicht geaendert hat, bekommt keine
#: neue Nummer. Eine feste Zahl an dieser Stelle wuerde irgendwann eine
#: falsche Version in die Fussnote schreiben.
app_version: str = "unbekannt",
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,
@@ -1456,17 +1456,23 @@
Block, Groesse mit dem Griff unten rechts, entfernen mit dem Kreuz. Block, Groesse mit dem Griff unten rechts, entfernen mit dem Kreuz.
"touch-action: none" ist auf dem iPad die Bedingung dafuer, dass ein Finger "touch-action: none" ist auf dem iPad die Bedingung dafuer, dass ein Finger
zieht statt die Seite zu scrollen. */ zieht statt die Seite zu scrollen. */
/* Die Marke ist genau die Zeichnung: ihre Unterkante liegt auf dem
Ankerpunkt -- dieselbe Kante, an der pdf-lib das Bild setzt. Der Text
haengt darunter und darf die Hoehe nicht mitbestimmen. */
.viewer__unterschrift { .viewer__unterschrift {
position: absolute; position: absolute;
z-index: 3; z-index: 3;
cursor: grab; cursor: grab;
touch-action: none; touch-action: none;
border: 1px dashed transparent; /* "outline" statt "border": ein Rahmen haette die Marke um zwei Pixel
groesser gemacht und die Bildkante vom Ankerpunkt weggeschoben. */
outline: 1px dashed transparent;
outline-offset: 1px;
} }
.viewer__unterschrift:hover, .viewer__unterschrift:hover,
.viewer__unterschrift.is-fassend { .viewer__unterschrift.is-fassend {
border-color: var(--accent); outline-color: var(--accent);
cursor: grabbing; cursor: grabbing;
} }
@@ -1482,6 +1488,12 @@
wie im PDF. Waere er so breit wie der Block, zeigte die Vorschau etwas wie im PDF. Waere er so breit wie der Block, zeigte die Vorschau etwas
anderes als die Datei. */ anderes als die Datei. */
.viewer__unterschrifttext { .viewer__unterschrifttext {
/* Unter der Zeichnung, nicht in ihr: "top: 100%" setzt die Oberkante des
Strichs auf die Bildkante -- genau dort zieht ihn auch pdf-lib. Abstand,
Strichstaerke und Innenabstand rechnet das Skript in Punkten. */
position: absolute;
top: 100%;
left: 0;
/* Helvetica: dieselbe Schrift, die das PDF benutzt (StandardFonts.Helvetica). /* Helvetica: dieselbe Schrift, die das PDF benutzt (StandardFonts.Helvetica).
Mit der Hausschrift sah die Vorschau anders aus als das Ergebnis. Die Mit der Hausschrift sah die Vorschau anders aus als das Ergebnis. Die
Groesse setzt das Skript je Unterschrift -- hier steht nur ein Rueckfall. */ Groesse setzt das Skript je Unterschrift -- hier steht nur ein Rueckfall. */
@@ -1493,7 +1505,6 @@
line-height: 1.15; line-height: 1.15;
color: #111; color: #111;
border-top: 1px solid rgba(0, 0, 0, 0.85); border-top: 1px solid rgba(0, 0, 0, 0.85);
padding-top: 1px;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
width: max-content; width: max-content;
@@ -297,9 +297,9 @@ async function nachbearbeiten(
const textbreite = Math.max( const textbreite = Math.max(
...zeilen.map((zeile) => schrift.widthOfTextAtSize(zeile, groesse)) ...zeilen.map((zeile) => schrift.widthOfTextAtSize(zeile, groesse))
); );
let unten = eintrag.y - 2; let unten = eintrag.y - UNTERSCHRIFT.textY;
for (const zeile of zeilen) { for (const zeile of zeilen) {
unten -= groesse * 1.15; unten -= groesse * UNTERSCHRIFT.zeile;
seite.drawText(zeile, { seite.drawText(zeile, {
x: eintrag.x, x: eintrag.x,
y: unten, y: unten,
@@ -312,9 +312,9 @@ async function nachbearbeiten(
// erkennbar zu bleiben: ein halber Punkt in hellem Grau ging zwischen // erkennbar zu bleiben: ein halber Punkt in hellem Grau ging zwischen
// den Tabellenlinien schlicht unter. // den Tabellenlinien schlicht unter.
seite.drawLine({ seite.drawLine({
start: { x: eintrag.x, y: eintrag.y - 1.5 }, start: { x: eintrag.x, y: eintrag.y - UNTERSCHRIFT.strichY },
end: { x: eintrag.x + textbreite, y: eintrag.y - 1.5 }, end: { x: eintrag.x + textbreite, y: eintrag.y - UNTERSCHRIFT.strichY },
thickness: 0.9, thickness: UNTERSCHRIFT.strich,
color: PDFLib.rgb(0.15, 0.15, 0.15), color: PDFLib.rgb(0.15, 0.15, 0.15),
}); });
} }
@@ -342,6 +342,71 @@ async function nachbearbeiten(
return dokument.save(); return dokument.save();
} }
/** Masse des Textblocks unter einer Unterschrift -- in PDF-Punkten.
*
* Die Vorschau rechnet dieselben Zahlen in Pixel um. Standen sie zweimal im
* Code, liefen sie auseinander: die Marke haengt mit ihrer **Unterkante** am
* Ankerpunkt, das PDF setzt dort die **Unterkante des Bildes** und schreibt
* den Text darunter. Beim Setzen sass der Block deshalb um die Texthoehe
* hoeher als in der gespeicherten Datei.
*/
const UNTERSCHRIFT = {
//: Mitte des Strichs unter der Bildkante.
strichY: 1.5,
//: Staerke des Strichs.
strich: 0.9,
//: Bildkante bis zur ersten Grundlinie, vor dem Zeilenabstand.
textY: 2,
//: Zeilenabstand als Vielfaches der Schriftgroesse.
zeile: 1.15,
//: Oberkante der Zeilenbox bis zur Grundlinie, in "em" -- nur ein
//: Rueckfall: gewoehnlich misst "grundlinie()" das am echten Text nach.
//: (Helvetica/Arial: Ascender 0,905 plus halber Durchschuss.)
grundlinie: 0.9215,
};
//: Gemessene Grundlinien, je dargestellter Schriftgroesse in Pixeln.
const grundlinien = new Map();
/** Abstand von der Oberkante der Zeilenbox zur Grundlinie, in "em".
*
* Gemessen statt geraten, und bei genau der Pixelgroesse, die auch auf dem
* Schirm steht. Zwei Gruende: welche Datei hinter "Helvetica" steckt,
* entscheidet das Geraet -- auf einem iPad eine andere als auf einem
* Windows-Rechner, mit anderen Oberlaengen. Und der Browser rundet die
* Schriftmasse auf ganze Pixel: bei acht Punkt Schrift verschiebt das die
* Zeile um einen halben Pixel gegenueber der Rechnung. pdf-lib rundet nicht.
*
* Eine leere Inline-Box sitzt auf der Grundlinie -- ihre Lage verraet also,
* wo der Text steht.
*/
function grundlinie(px) {
const schluessel = px.toFixed(1);
const gemerkt = grundlinien.get(schluessel);
if (gemerkt !== undefined) return gemerkt;
let anteil = UNTERSCHRIFT.grundlinie;
try {
const probe = document.createElement("div");
probe.className = "viewer__unterschrifttext";
probe.style.cssText =
"position:absolute;left:-9999px;top:0;visibility:hidden;border:0;padding:0;";
probe.style.fontSize = `${px}px`;
probe.style.lineHeight = String(UNTERSCHRIFT.zeile);
const lot = document.createElement("span");
lot.style.cssText = "display:inline-block;width:0;height:0;";
probe.append(document.createTextNode("Hg"), lot);
document.body.append(probe);
const oben = probe.getBoundingClientRect().top;
const auf = lot.getBoundingClientRect().top;
probe.remove();
if (auf > oben) anteil = (auf - oben) / px;
} catch (fehler) {
console.warn("Grundlinie nicht messbar -- Rueckfall auf Helvetica", fehler);
}
grundlinien.set(schluessel, anteil);
return anteil;
}
/** Schriftgroesse des Namenszugs unter einer Unterschrift dieser Breite. /** Schriftgroesse des Namenszugs unter einer Unterschrift dieser Breite.
* *
* Ein Achtzehntel: bei den 150 Punkt der vollen Breite sind das gut acht * Ein Achtzehntel: bei den 150 Punkt der vollen Breite sind das gut acht
@@ -716,17 +781,42 @@ async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true }
seitenEl.append(marke); seitenEl.append(marke);
const zeichnen = () => { const zeichnen = () => {
// Die Marke ist genau die Zeichnung: ihre Unterkante liegt auf dem
// Ankerpunkt, so wie im PDF. Der Text haengt darunter (siehe
// Gestaltung: absolut, "top: 100%") und zaehlt nicht zur Hoehe --
// sonst saesse der Block in der Vorschau um die Texthoehe zu hoch.
marke.style.left = `${(eintrag.x / blick.width) * 100}%`; marke.style.left = `${(eintrag.x / blick.width) * 100}%`;
marke.style.bottom = `${(eintrag.y / blick.height) * 100}%`; marke.style.bottom = `${(eintrag.y / blick.height) * 100}%`;
marke.style.width = `${(eintrag.breite / blick.width) * 100}%`; marke.style.width = `${(eintrag.breite / blick.width) * 100}%`;
// Die Schriftgroesse wird gerechnet, nicht vom Stylesheet abgeleitet: // Alles Weitere wird gerechnet, nicht vom Stylesheet abgeleitet:
// Punkte mal Pixel-je-Punkt der gerade dargestellten Seite. Ueber // Punkte mal Pixel-je-Punkt der gerade dargestellten Seite. Ueber
// Container-Einheiten ("cqw") ging das auch -- aber nur dort, wo der // Container-Einheiten ("cqw") ging das auch -- aber nur dort, wo der
// Browser sie kennt; wo nicht, erbte der Block die Schriftgroesse der // Browser sie kennt; wo nicht, erbte der Block die Schriftgroesse der
// Seite, und der Text war um die Haelfte zu breit. Genau das war der // Seite, und der Text war um die Haelfte zu breit.
// Unterschied zwischen Vorschau und Datei.
const proPunkt = seitenEl.getBoundingClientRect().width / blick.width; const proPunkt = seitenEl.getBoundingClientRect().width / blick.width;
text.style.fontSize = `${schriftgroesse(eintrag.breite) * proPunkt}px`; const groesse = schriftgroesse(eintrag.breite);
const inPixeln = groesse * proPunkt;
const oben = UNTERSCHRIFT.strichY - UNTERSCHRIFT.strich / 2;
text.style.fontSize = `${inPixeln}px`;
text.style.lineHeight = String(UNTERSCHRIFT.zeile);
text.style.marginTop = `${oben * proPunkt}px`;
text.style.borderTopWidth = `${UNTERSCHRIFT.strich * proPunkt}px`;
// Rahmenbreiten rundet der Browser auf ganze Geraetepixel: aus 1,21
// wird 1. Mit dem gerechneten Wert sass die erste Zeile deshalb einen
// Fuenftel Punkt zu hoch -- also mit dem tatsaechlich benutzten rechnen.
const strich =
(parseFloat(getComputedStyle(text).borderTopWidth) ||
UNTERSCHRIFT.strich * proPunkt) / proPunkt;
// Oberkante des Strichs, die Strichstaerke, dann so viel Luft, dass die
// erste Grundlinie dort sitzt, wo pdf-lib sie setzt: textY plus einen
// Zeilenabstand unter der Bildkante.
const innen =
UNTERSCHRIFT.textY +
UNTERSCHRIFT.zeile * groesse -
oben -
strich -
grundlinie(inPixeln) * groesse;
text.style.paddingTop = `${Math.max(0, innen) * proPunkt}px`;
}; };
// Beim Zoomen aendert sich Pixel-je-Punkt -- dann neu rechnen. // Beim Zoomen aendert sich Pixel-je-Punkt -- dann neu rechnen.
neuzeichnen.add(zeichnen); neuzeichnen.add(zeichnen);
@@ -764,8 +854,8 @@ async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true }
} else { } else {
eintrag.x = Math.max(2, start.x + (zug.clientX - startX) / proPunkt); eintrag.x = Math.max(2, start.x + (zug.clientX - startX) / proPunkt);
eintrag.y = Math.max( eintrag.y = Math.max(
// Platz fuer die Textzeilen unter der Zeichnung. // Platz fuer die beiden Textzeilen unter der Zeichnung.
schriftgroesse(eintrag.breite) * 2.5, UNTERSCHRIFT.textY + 2 * UNTERSCHRIFT.zeile * schriftgroesse(eintrag.breite),
start.y - (zug.clientY - startY) / proPunkt start.y - (zug.clientY - startY) / proPunkt
); );
} }