frontend: Diagramm-Copy als PNG + text/html-Flavor (LibreOffice-lesbar)

Das Diagramm wird bewusst als PNG kopiert. Zusätzlich zum image/png-Flavor
wird eine text/html-Variante mit eingebettetem PNG geschrieben, die
Office-Programme wie LibreOffice Writer bevorzugt einbetten — damit ist das
Bild dort einfügbar. Der Tooltip nennt jetzt das Format ("PNG-Bild in die
Zwischenablage"). SPEC §9 entsprechend ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-21 12:44:47 +02:00
co-authored by Claude Opus 4.8
parent 57fb72cbe4
commit 508de56628
2 changed files with 28 additions and 17 deletions
+6 -3
View File
@@ -140,9 +140,12 @@ kopierbar (grafisches Format, in Dokumente/Chats einfügbar):
- Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges - Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges
SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als
**PNG** (`image/png`) in die Zwischenablage gelegt. Fällt der Bild-Clipboard **PNG** in die Zwischenablage gelegt. Es werden zwei Flavors geschrieben:
aus (fehlende `ClipboardItem`-Unterstützung), wird der **SVG-Quelltext** `image/png` (das eigentliche Bild) und `text/html` mit eingebettetem PNG
kopiert (ebenfalls ein Grafikformat). (`<img src="data:image/png;…">`) — Office-Programme wie LibreOffice Writer
bevorzugen den HTML-Flavor und betten das Bild dann korrekt ein. Fällt der
Bild-Clipboard ganz aus (fehlende `ClipboardItem`-Unterstützung), wird der
**SVG-Quelltext** kopiert (ebenfalls ein Grafikformat).
- Übernommen werden Knotenfarben (Status §4), Größen-Badge, Tags und der - Übernommen werden Knotenfarben (Status §4), Größen-Badge, Tags und der
Geister-Knoten; die Verbindungslinien werden je Gate neu gezogen Geister-Knoten; die Verbindungslinien werden je Gate neu gezogen
(und = durchgezogen Tinte, oder = gestrichelt Grau) und treffen die Knoten (und = durchgezogen Tinte, oder = gestrichelt Grau) und treffen die Knoten
+22 -14
View File
@@ -564,7 +564,7 @@
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span> <span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span>
</label> </label>
</div> </div>
<button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als Bild kopieren" aria-label="Diagramm als Bild kopieren"> <button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als PNG-Bild in die Zwischenablage kopieren" aria-label="Diagramm als PNG-Bild in die Zwischenablage kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> <svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button> </button>
@@ -825,7 +825,7 @@ function diagramToSvg(){
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'IBM Plex Sans',system-ui,sans-serif">${parts.join('')}</svg>`; const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'IBM Plex Sans',system-ui,sans-serif">${parts.join('')}</svg>`;
return {svg, W, H}; return {svg, W, H};
} }
function svgToPngBlob(svg, W, H, scale){ function svgToPng(svg, W, H, scale){
return new Promise(resolve => { return new Promise(resolve => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
@@ -834,20 +834,28 @@ function svgToPngBlob(svg, W, H, scale){
const ctx = c.getContext('2d'); const ctx = c.getContext('2d');
ctx.scale(scale, scale); ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
c.toBlob(b => resolve(b), 'image/png'); c.toBlob(b => resolve({blob:b, dataUrl:c.toDataURL('image/png')}), 'image/png');
}; };
img.onerror = () => resolve(null); img.onerror = () => resolve(null);
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}); });
} }
/* Kopiert das Diagramm als PNG-Bild. Zusätzlich wird eine text/html-Variante
mit eingebettetem PNG mitgegeben — Office-Programme wie LibreOffice Writer
bevorzugen den HTML-Flavor und betten das Bild dann korrekt ein. */
async function copyDiagramImage(){ async function copyDiagramImage(){
const {svg, W, H} = diagramToSvg(); const {svg, W, H} = diagramToSvg();
const blob = await svgToPngBlob(svg, W, H, 2); const png = await svgToPng(svg, W, H, 2);
if(blob && navigator.clipboard && window.ClipboardItem){ if(png && png.blob && navigator.clipboard && window.ClipboardItem){
const html = new Blob(
[`<img src="${png.dataUrl}" width="${W}" height="${H}" alt="Werkbaum-Diagramm">`],
{type:'text/html'});
try{ try{
await navigator.clipboard.write([new ClipboardItem({'image/png': blob})]); await navigator.clipboard.write([new ClipboardItem({'image/png':png.blob, 'text/html':html})]);
return; return;
}catch(_){} }catch(_){
try{ await navigator.clipboard.write([new ClipboardItem({'image/png':png.blob})]); return; }catch(_){}
}
} }
await writeClipboard(svg); /* Fallback: SVG-Quelltext (ebenfalls Grafik) */ await writeClipboard(svg); /* Fallback: SVG-Quelltext (ebenfalls Grafik) */
} }
@@ -1028,7 +1036,7 @@ const I18N = {
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm", editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren", copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
copyDiagramTooltip:"Diagramm als Bild kopieren", copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
minimize:"minimieren", normal:"normal", maximize:"maximieren", minimize:"minimieren", normal:"normal", maximize:"maximieren",
agenda:"Agenda", discarded:"verworfene", agenda:"Agenda", discarded:"verworfene",
modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor", modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor",
@@ -1057,7 +1065,7 @@ const I18N = {
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", diagramTitle:"Diagram", editorTitle:"Structure (text)", diagramTitle:"Diagram",
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard", copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
copyDiagramTooltip:"Copy diagram as an image", copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
minimize:"minimize", normal:"normal", maximize:"maximize", minimize:"minimize", normal:"normal", maximize:"maximize",
agenda:"Legend", discarded:"discarded", agenda:"Legend", discarded:"discarded",
modeHorizontal:"Horizontal org chart, diagram above the editor", modeHorizontal:"Horizontal org chart, diagram above the editor",
@@ -1086,7 +1094,7 @@ const I18N = {
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).", brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama", editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles", copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
copyDiagramTooltip:"Copiar el diagrama como imagen", copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
minimize:"minimizar", normal:"normal", maximize:"maximizar", minimize:"minimizar", normal:"normal", maximize:"maximizar",
agenda:"Leyenda", discarded:"descartados", agenda:"Leyenda", discarded:"descartados",
modeHorizontal:"Horizontal organigrama, diagrama sobre el editor", modeHorizontal:"Horizontal organigrama, diagrama sobre el editor",
@@ -1115,7 +1123,7 @@ const I18N = {
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).", brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme", editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers", copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
copyDiagramTooltip:"Copier le diagramme comme image", copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
minimize:"réduire", normal:"normal", maximize:"agrandir", minimize:"réduire", normal:"normal", maximize:"agrandir",
agenda:"Légende", discarded:"abandonnés", agenda:"Légende", discarded:"abandonnés",
modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur", modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur",
@@ -1144,7 +1152,7 @@ const I18N = {
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram", editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka", copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
copyDiagramTooltip:"Kopiuj diagram jako obraz", copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj", minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj",
agenda:"Legenda", discarded:"odrzucone", agenda:"Legenda", discarded:"odrzucone",
modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem", modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem",
@@ -1173,7 +1181,7 @@ const I18N = {
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма", editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена", copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как изображение", copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
minimize:"свернуть", normal:"обычный", maximize:"развернуть", minimize:"свернуть", normal:"обычный", maximize:"развернуть",
agenda:"Легенда", discarded:"отклонённые", agenda:"Легенда", discarded:"отклонённые",
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором", modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
@@ -1202,7 +1210,7 @@ const I18N = {
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख", editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें", copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को छवि के रूप में कॉपी करें", copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें", minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें",
agenda:"लेजेंड", discarded:"अस्वीकृत", agenda:"लेजेंड", discarded:"अस्वीकृत",
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख", modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",