Zurück zum Blog

Bilder fehlen beim Markdown-zu-Word-Export? So haben wir Cross-Origin- und WebP-Kompatibilitätsprobleme gelöst

MD-TO Team

Beim Aufbau der Markdown-zu-Word-Exportfunktion bei md-to.com stießen wir auf einen frustrierenden Fehler: Bilder im Markdown-Inhalt der Benutzer verschwanden nach dem Export in Word.

Die Ursache lag in zwei Schichten: Browser-Sicherheitsrichtlinie (CORS) und Bildformat-Kompatibilität (WebP). Dieser Beitrag dokumentiert den Debugging-Prozess und unsere endgültige Lösung.


Das Symptom

Ein Benutzer fügt Markdown wie dieses in den Editor ein:

![Editor-Oberfläche](https://example.com/screenshot.webp)
![Seitensymbol](https://example.com/icon.png)

Die Vorschau rendert beide Bilder einwandfrei. Aber in der exportierten Word-Datei fehlen beide Bilder — ersetzt durch Platzhaltertext wie [Editor-Oberfläche] und [Seitensymbol].


Problem 1: Word unterstützt kein WebP

Ursache

Wir verwenden die docx-Bibliothek zur Word-Dokumenterzeugung. Ihre ImageRun-Komponente unterstützt nur vier Bildformate:

type DocxImageType = 'jpg' | 'png' | 'gif' | 'bmp';

Moderne Websites verwenden intensiv WebP (25-35 % kleiner als PNG). Wenn unser Code auf ein nicht unterstütztes Format stieß, warf er einen Unsupported image type-Fehler und das Bild wurde stillschweigend verworfen.

Lösung: Automatische Konvertierung über Canvas

Browser rendern WebP nativ, daher können wir die Canvas-API verwenden, um es in PNG zu konvertieren:

function loadImageViaCanvas(
  source: Blob | string,
): Promise<{ blob: Blob; width: number; height: number }> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const isBlobSource = source instanceof Blob;
    if (!isBlobSource) {
      img.crossOrigin = 'anonymous';
    }
    const blobUrl = isBlobSource ? URL.createObjectURL(source) : null;

    img.onload = () => {
      if (blobUrl) URL.revokeObjectURL(blobUrl);
      const { naturalWidth: width, naturalHeight: height } = img;
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d')!;
      ctx.drawImage(img, 0, 0);
      canvas.toBlob(
        (pngBlob) => {
          if (pngBlob) resolve({ blob: pngBlob, width, height });
          else reject(new Error('Canvas toBlob failed'));
        },
        'image/png',
      );
    };
    img.onerror = () => reject(new Error('Failed to load image'));
    img.src = blobUrl ?? (source as string);
  });
}

Diese Funktion akzeptiert zwei Eingabetypen:

  • Blob: für reine Formatkonvertierung bereits abgerufener Bilder
  • URL-String: für CORS-Fallback (mehr dazu unten)

Unabhängig vom Eingabeformat (WebP, AVIF usw.) ist die Ausgabe immer PNG — was Word problemlos verarbeitet.


Problem 2: CORS blockiert Cross-Origin-Bildabrufe

Nach der Behebung des WebP-Problems entdeckten wir ein grundlegenderes Problem: Cross-Origin-Bilder konnten überhaupt nicht abgerufen werden.

Ursache

Unser Code verwendet fetch() zum Abrufen von Bilddaten:

const response = await fetch(imageUrl);
const blob = await response.blob();

Wenn die Bild-URL auf einer anderen Domain liegt als die Website (z.B. Website ist md-to.com, Bild ist auf example.com), blockiert die CORS (Cross-Origin Resource Sharing)-Richtlinie des Browsers die Anfrage:

Access to fetch at 'https://example.com/image.png'
from origin 'https://md-to.com' has been blocked by CORS policy

Warum funktioniert die Vorschau, aber nicht der Export?

Weil der Browser unterschiedliche Sicherheitsstufen für verschiedene Operationen anwendet:

OperationCross-Origin erlaubt?Warum
<img src="extern"> RenderingJaBrowser rendert Bilder ohne CORS
fetch() DatenzugriffBlockiertJS-Zugriff auf Binärdaten erfordert CORS
<img crossOrigin> + Canvas ExportBlockiertKontaminiertes Canvas kann keine Daten exportieren
fetch({mode: 'no-cors'})UnlesbarGibt leeren Body zurück

Die Vorschau verwendet <img>-Tags (Cross-Origin-Rendering erlaubt), während der Export JavaScript benötigt, um die Binärdaten des Bildes zu lesen (durch CORS blockiert). Deshalb können Sie es „sehen, aber nicht exportieren”.

Warum Base64 auch nicht hilft

Man könnte denken: „Konvertiere die Bilder einfach in Base64!”

Aber Base64 ist nur ein Kodierungsformat, keine Datengewinnungsmethode. Das Problem ist nicht „in welchem Format speichern” — es ist „der Browser lässt den Zugriff auf die Rohdaten nicht zu.” Ob Sie in Base64, ArrayBuffer oder etwas anderes kodieren möchten — der erste Schritt ist das Abrufen der Daten — und CORS blockiert das vollständig.

Lösung: Serverseitiger Bildproxy

Da es keine browserseitige Umgehung gibt, lassen wir den Server die Bilder abrufen. Server haben keine CORS-Einschränkungen und können frei jede URL anfordern.

Wir haben eine leichtgewichtige Edge Function als Bildproxy erstellt:

Browser → /api/image-proxy?url=https://example.com/image.png → Unser Server → example.com
                                                                ↑ Keine CORS-Einschränkung

Proxy-Funktion (am Edge deployed):

export async function onRequest({ request }) {
  const { searchParams } = new URL(request.url);
  const target = searchParams.get('url');

  // Sicherheit: nur http(s), nur Bild-Typen, Größenlimit
  const upstream = await fetch(target);
  const contentType = upstream.headers.get('content-type');

  return new Response(await upstream.arrayBuffer(), {
    headers: {
      'Content-Type': contentType,
      'Cache-Control': 'public, max-age=86400',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

Clientseitiges Auto-Routing:

function resolveImageSrc(src: string): string {
  const resolved = new URL(src, window.location.href).toString();

  // Cross-Origin-Bilder über Proxy leiten
  if (new URL(resolved).origin !== window.location.origin) {
    return `/api/image-proxy?url=${encodeURIComponent(resolved)}`;
  }

  return resolved;
}

Alle Cross-Origin-Bildanfragen werden nun zu Same-Origin-Anfragen — keine CORS-Probleme mehr.


Endgültige Architektur

Bild-URL im Markdown


  resolveImageSrc()

        ├── Same-Origin → Direkter Fetch

        └── Cross-Origin → /api/image-proxy?url=xxx


                        Edge Function leitet weiter


                          Blob erhalten


                    getDocxImageType() Format prüfen

                    ┌─────────┴──────────┐
                    ▼                    ▼
              jpg/png/gif/bmp      webp/avif/andere
              Direkt einbetten     Canvas → PNG → Einbetten

Entwicklungsumgebung

Eine passende Proxy-Middleware wurde zum Vite-Entwicklungsserver hinzugefügt, um konsistentes Verhalten zwischen Entwicklung und Produktion sicherzustellen.

Sicherheitsmaßnahmen

Der Bildproxy ist kein uneingeschränkter offener Proxy:

  • Erlaubt nur http://- und https://-Protokolle
  • Leitet nur Bild-Content-Types weiter (png/jpeg/gif/bmp/webp/avif/svg)
  • 10 MB Größenlimit pro Bild
  • 24-Stunden-Cache auf Antworten

Zusammenfassung

ProblemUrsacheLösung
WebP-Bilder erscheinen nicht in Worddocx-Bibliothek unterstützt kein WebPCanvas-API konvertiert in PNG
Cross-Origin-Bilder können nicht abgerufen werdenBrowser-CORS-SicherheitsrichtlinieServerseitige Edge-Function-Proxy
Bilder scheitern auch in der Entwicklungsumgebunglocalhost → externe Domain ist Cross-OriginVite-Entwicklungsserver-Proxy-Middleware

Kernaussage: Das Sicherheitsmodell des Browsers unterscheidet zwischen „Rendering” und „Datenzugriff”. Nur weil <img> ein Cross-Origin-Bild anzeigen kann, bedeutet das nicht, dass JavaScript seine Daten lesen kann. Wenn Sie clientseitigen Zugriff auf Cross-Origin-Ressourcen benötigen, ist ein serverseitiger Proxy praktisch der einzige zuverlässige Ansatz.


Möchten Sie es ausprobieren? Besuchen Sie md-to.com Markdown zu Word, fügen Sie Markdown mit externen Bildern ein und exportieren Sie.