diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index cbb3f5f..92ebfb6 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.7.7" +version = "2.7.8" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index cf574e0..317a50c 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] 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" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/app_factory.py b/packages/tesm-core/src/tesm_core/app_factory.py index e9431d2..77fa1b4 100644 --- a/packages/tesm-core/src/tesm_core/app_factory.py +++ b/packages/tesm-core/src/tesm_core/app_factory.py @@ -148,7 +148,11 @@ def create_base_app( #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Anwendungen. 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, ...] = (), enable_license: bool = True, template_folder: str | None = None, diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css index a71cc2f..278ccf4 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css @@ -1456,17 +1456,23 @@ Block, Groesse mit dem Griff unten rechts, entfernen mit dem Kreuz. "touch-action: none" ist auf dem iPad die Bedingung dafuer, dass ein Finger 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 { position: absolute; z-index: 3; cursor: grab; 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.is-fassend { - border-color: var(--accent); + outline-color: var(--accent); cursor: grabbing; } @@ -1482,6 +1488,12 @@ wie im PDF. Waere er so breit wie der Block, zeigte die Vorschau etwas anderes als die Datei. */ .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). Mit der Hausschrift sah die Vorschau anders aus als das Ergebnis. Die Groesse setzt das Skript je Unterschrift -- hier steht nur ein Rueckfall. */ @@ -1493,7 +1505,6 @@ line-height: 1.15; color: #111; border-top: 1px solid rgba(0, 0, 0, 0.85); - padding-top: 1px; pointer-events: none; white-space: nowrap; width: max-content; diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js index 1af6587..8b2c120 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js @@ -297,9 +297,9 @@ async function nachbearbeiten( const textbreite = Math.max( ...zeilen.map((zeile) => schrift.widthOfTextAtSize(zeile, groesse)) ); - let unten = eintrag.y - 2; + let unten = eintrag.y - UNTERSCHRIFT.textY; for (const zeile of zeilen) { - unten -= groesse * 1.15; + unten -= groesse * UNTERSCHRIFT.zeile; seite.drawText(zeile, { x: eintrag.x, y: unten, @@ -312,9 +312,9 @@ async function nachbearbeiten( // erkennbar zu bleiben: ein halber Punkt in hellem Grau ging zwischen // den Tabellenlinien schlicht unter. seite.drawLine({ - start: { x: eintrag.x, y: eintrag.y - 1.5 }, - end: { x: eintrag.x + textbreite, y: eintrag.y - 1.5 }, - thickness: 0.9, + start: { x: eintrag.x, y: eintrag.y - UNTERSCHRIFT.strichY }, + end: { x: eintrag.x + textbreite, y: eintrag.y - UNTERSCHRIFT.strichY }, + thickness: UNTERSCHRIFT.strich, color: PDFLib.rgb(0.15, 0.15, 0.15), }); } @@ -342,6 +342,71 @@ async function nachbearbeiten( 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. * * 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); 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.bottom = `${(eintrag.y / blick.height) * 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 // Container-Einheiten ("cqw") ging das auch -- aber nur dort, wo 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 - // Unterschied zwischen Vorschau und Datei. + // Seite, und der Text war um die Haelfte zu breit. 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. neuzeichnen.add(zeichnen); @@ -764,8 +854,8 @@ async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true } } else { eintrag.x = Math.max(2, start.x + (zug.clientX - startX) / proPunkt); eintrag.y = Math.max( - // Platz fuer die Textzeilen unter der Zeichnung. - schriftgroesse(eintrag.breite) * 2.5, + // Platz fuer die beiden Textzeilen unter der Zeichnung. + UNTERSCHRIFT.textY + 2 * UNTERSCHRIFT.zeile * schriftgroesse(eintrag.breite), start.y - (zug.clientY - startY) / proPunkt ); }