Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  image-size.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/**
 * Image dimension helpers for QQ Bot markdown image syntax.
 *
 * QQ Bot markdown images use `![#widthpx #heightpx](url)`.
 */

import { Buffer } from "node:buffer";
import { getPlatformAdapter } from "../adapter/index.js";
import type { SsrfPolicyConfig } from "../adapter/types.js";
import { formatErrorMessage } from "./format.js";
import { debugLog } from "./log.js";

export interface ImageSize {
  width: number;
  height: number;
}

/** Default dimensions used when probing fails. */
export const DEFAULT_IMAGE_SIZE: ImageSize = { width: 512, height: 512 };

/**
 * Parse image dimensions from the PNG header.
 */
function parsePngSize(buffer: Buffer): ImageSize | null {
  // PNG signature: 89 50 4E 47 0D 0A 1A 0A
  if (buffer.length < 24) {
    return null;
  }
  if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
    return null;
  }
  // The IHDR chunk begins at byte 8, with width/height at 16..23.
  const width = buffer.readUInt32BE(16);
  const height = buffer.readUInt32BE(20);
  return { width, height };
}

/** Parse image dimensions from JPEG SOF0/SOF2 markers. */
function parseJpegSize(buffer: Buffer): ImageSize | null {
  // JPEG signature: FF D8 FF
  if (buffer.length < 4) {
    return null;
  }
  if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
    return null;
  }

  let offset = 2;
  while (offset < buffer.length - 9) {
    if (buffer[offset] !== 0xff) {
      offset++;
      continue;
    }

    const marker = buffer[offset + 1];
    // SOF0 (0xC0) and SOF2 (0xC2) contain dimensions.
    if (marker === 0xc0 || marker === 0xc2) {
      // Layout: FF C0 length(2) precision(1) height(2) width(2)
      if (offset + 9 <= buffer.length) {
        const height = buffer.readUInt16BE(offset + 5);
        const width = buffer.readUInt16BE(offset + 7);
        return { width, height };
      }
    }

    // Skip the current block.
    if (offset + 3 < buffer.length) {
      const blockLength = buffer.readUInt16BE(offset + 2);
      offset += 2 + blockLength;
    } else {
      break;
    }
  }

  return null;
}

/** Parse image dimensions from the GIF header. */
function parseGifSize(buffer: Buffer): ImageSize | null {
  if (buffer.length < 10) {
    return null;
  }
  const signature = buffer.toString("ascii", 0, 6);
  if (signature !== "GIF87a" && signature !== "GIF89a") {
    return null;
  }
  const width = buffer.readUInt16LE(6);
  const height = buffer.readUInt16LE(8);
  return { width, height };
}

/** Parse image dimensions from WebP headers. */
function parseWebpSize(buffer: Buffer): ImageSize | null {
  if (buffer.length < 30) {
    return null;
  }

  // Check the RIFF and WEBP signatures.
  const riff = buffer.toString("ascii", 0, 4);
  const webp = buffer.toString("ascii", 8, 12);
  if (riff !== "RIFF" || webp !== "WEBP") {
    return null;
  }

  const chunkType = buffer.toString("ascii", 12, 16);

  // VP8 (lossy)
  if (chunkType === "VP8 ") {
    // The VP8 frame header starts at byte 23 and uses the 9D 01 2A signature.
    if (buffer.length >= 30 && buffer[23] === 0x9d && buffer[24] === 0x01 && buffer[25] === 0x2a) {
      const width = buffer.readUInt16LE(26) & 0x3fff;
      const height = buffer.readUInt16LE(28) & 0x3fff;
      return { width, height };
    }
  }

  // VP8L (lossless)
  if (chunkType === "VP8L") {
    // VP8L signature: 0x2F
    if (buffer.length >= 25 && buffer[20] === 0x2f) {
      const bits = buffer.readUInt32LE(21);
      const width = (bits & 0x3fff) + 1;
      const height = ((bits >> 14) & 0x3fff) + 1;
      return { width, height };
    }
  }

  // VP8X (extended format)
  if (chunkType === "VP8X") {
    if (buffer.length >= 30) {
      // Width and height live at 24..26 and 27..29 as 24-bit little-endian values.
      const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
      const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
      return { width, height };
    }
  }

  return null;
}

/** Parse image dimensions from raw image bytes. */
export function parseImageSize(buffer: Buffer): ImageSize | null {
  // Try each supported image format in sequence.
  return (
    parsePngSize(buffer) ?? parseJpegSize(buffer) ?? parseGifSize(buffer) ?? parseWebpSize(buffer)
  );
}

/**
 * SSRF policy for image-dimension probing.  Generic public-network-only blocking
 * (no hostname allowlist) because markdown image URLs can legitimately point to
 * any public host, not just QQ-owned CDNs.
 */
const IMAGE_PROBE_SSRF_POLICY: SsrfPolicyConfig = {};

/**
 * Fetch image dimensions from a public URL using only the first 64 KB.
 *
 * Uses {@link fetchRemoteMedia} with SSRF guard to block probes against
 * private/reserved/loopback/link-local/metadata destinations.
 */
export async function getImageSizeFromUrl(
  url: string,
  timeoutMs = 5000,
): Promise<ImageSize | null> {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const { buffer } = await getPlatformAdapter().fetchMedia({
        url,
        maxBytes: 65_536,
        maxRedirects: 0,
        ssrfPolicy: IMAGE_PROBE_SSRF_POLICY,
        requestInit: {
          signal: controller.signal,
          headers: {
            Range: "bytes=0-65535",
            "User-Agent": "QQBot-Image-Size-Detector/1.0",
          },
        },
      });

      const size = parseImageSize(buffer);
      if (size) {
        debugLog(
          `[image-size] Got size from URL: ${size.width}x${size.height} - ${url.slice(0, 60)}...`,
        );
      }
      return size;
    } finally {
      clearTimeout(timeoutId);
    }
  } catch (err) {
    debugLog(`[image-size] Error fetching ${url.slice(0, 60)}...: ${formatErrorMessage(err)}`);
    return null;
  }
}

/** Parse image dimensions from a Base64 data URL. */
export function getImageSizeFromDataUrl(dataUrl: string): ImageSize | null {
  try {
    // Format: data:image/png;base64,xxxxx
    const matches = dataUrl.match(/^data:image\/[^;]+;base64,(.+)$/);
    if (!matches) {
      return null;
    }

    const base64Data = matches[1];
    const buffer = Buffer.from(base64Data, "base64");

    const size = parseImageSize(buffer);
    if (size) {
      debugLog(`[image-size] Got size from Base64: ${size.width}x${size.height}`);
    }

    return size;
  } catch (err) {
    debugLog(`[image-size] Error parsing Base64: ${formatErrorMessage(err)}`);
    return null;
  }
}

/**
 * Resolve image dimensions from either an HTTP URL or a Base64 data URL.
 */
export async function getImageSize(source: string): Promise<ImageSize | null> {
  if (source.startsWith("data:")) {
    return getImageSizeFromDataUrl(source);
  }

  if (source.startsWith("http://") || source.startsWith("https://")) {
    return getImageSizeFromUrl(source);
  }

  return null;
}

/** Format a markdown image with QQ Bot width/height annotations. */
export function formatQQBotMarkdownImage(url: string, size: ImageSize | null): string {
  const { width, height } = size ?? DEFAULT_IMAGE_SIZE;
  return `![#${width}px #${height}px](${url})`;
}

/** Return true when markdown already contains QQ Bot size annotations. */
export function hasQQBotImageSize(markdownImage: string): boolean {
  return /!\[#\d+px\s+#\d+px\]/.test(markdownImage);
}

/** Extract width and height from QQBot markdown image syntax: `![#Wpx #Hpx](url)`. */
export function extractQQBotImageSize(markdownImage: string): ImageSize | null {
  const match = markdownImage.match(/!\[#(\d+)px\s+#(\d+)px\]/);
  if (match) {
    return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
  }
  return null;
}

¤ Dauer der Verarbeitung: 0.17 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge