import type { Gradient, PPTElement, PPTShapeElement, PPTTableElement, PPTTextElement, Slide, SlideBackground } from '../types/slides'; /** * Painter's-algorithm text contrast repair for AI-imported decks. * * The query engine answers "what is painted under this text?" by sampling * points inside the text's bounds and walking the slide's element list from * the text downwards (elements render in array order, so the first hit while * walking backwards is the topmost layer below the text). A layer only * counts when it actually covers the sample: empty / transparent fills and * outline-only (border) shapes are see-through; filled shapes are hit-tested * against their path (ellipse corners are not a rectangle). Themed table * cells sit on zinc paper with an opaque ink header (same as the renderer). * Images are `unknown` unless a sampled color cache is supplied (AI import * prefetches overlap regions via `fast-average-color`). Patterns and charts stay * `unknown`. * * Texts whose worst-case WCAG contrast falls below {@link CONTRAST_TRIGGER} * are recolored with a stark black ↔ white inversion based on the * *background* polarity (dark fill → white ink, light fill → black ink). * Text that already has the right polarity is left alone — flipping white * numbers on a mid-teal chip to black "improves" the WCAG ratio (2.98 → 7) * but looks worse. Mid-tone "barely AA" greys of the *wrong* polarity are * still snapped to black/white; AI decks need decisive contrast. */ /** Fix texts below this ratio (WCAG AA minimum for large text). */ export declare const CONTRAST_TRIGGER = 3; /** Stark fixes (black/white) clear this comfortably; kept for callers/tests. */ export declare const CONTRAST_TARGET = 4.5; interface ThemeColors { backgroundColor: string; fontColor: string; } /** Canvas-space rectangle with a sampled average hex (from `fast-average-color`). */ export interface ImageRegionPaint { minX: number; maxX: number; minY: number; maxY: number; hex: string; } /** * Prefetched image paints for one slide. Without this, photographs stay * `unknown` and the fixer leaves overlapping text alone. */ export interface ImagePaintCache { /** Image element id → sampled text∩image regions. */ byElementId: Map; /** Slide image-background regions (cover-mapped), if any. */ slideBackground?: ImageRegionPaint[]; } export interface ContrastContext { images?: ImagePaintCache; } /** Flatten a gradient to a single representative color (average of stops). */ export declare const gradientAverageColor: (gradient: Gradient) => string; /** * Representative surface color of a slide background. Gradients average their * stops; images fall back to `fallback` (usually the theme background) because * the pixel paint is unknown without a sample cache. Translucent solids are * composited over white so polarity matches what the user actually sees. */ export declare const resolveSlideSurfaceColor: (background: SlideBackground | undefined, fallback?: string) => string; /** Effective opaque fill of one cell (explicit backcolor, else theme stripe). */ export declare const resolveTableCellFill: (el: PPTTableElement, row: number, col: number) => string | null; export interface BackgroundQueryResult { /** Distinct opaque colors found under the sampled points. */ colors: string[]; /** True when any sample hit a layer that can't be evaluated (image, …). */ unknown: boolean; } /** * Query engine: resolve what is painted under the element at `index`. * Samples the element's bounds and, per sample, returns the topmost paint of * the elements below it in painter order, falling back to the slide * background. */ export declare const queryBackgroundsUnder: (elements: PPTElement[], index: number, background: SlideBackground | undefined, theme: ThemeColors, ctx?: ContrastContext) => BackgroundQueryResult; /** * Return a readable replacement for `color` against every background, or * `null` when the original should be kept. * * Preferred ink follows the *background*: dark fill → white, light fill → * black. Text that already has that polarity is never flipped — white on a * mid-luminance teal chip is ~2.98:1 (just under {@link CONTRAST_TRIGGER}) * but inverting it to black is the numbered-chip regression. Wrong-polarity * text below the trigger snaps to the preferred ink (no muddy mid-tones). */ export declare const fixColorForBackgrounds: (color: string, backgrounds: string[]) => string | null; export declare const isDefaultInk: (color: string) => boolean; export type InkPolarity = '#ffffff' | '#000000'; /** * Canonical default ink for a set of surfaces (solid fill or every gradient stop). * * WCAG `mostReadable` prefers black on hot pink (`#e64980` 5.63 vs 3.73) even * though white also clears 3:1 and is what a dark slide should use. Rule: * 1. If only one of black/white meets {@link CONTRAST_TRIGGER} against every * surface, use that one. * 2. Otherwise (both pass, both fail, mixed stops) use perceived darkness of * the darkest surface — not max-contrast, not `isDark()`. * * Do **not** use {@link fixColorForBackgrounds} for defaults: that helper is * conservative AI-import repair and will keep `#333` on magenta because 3.4:1 * already passes the trigger. */ export declare const preferredInk: (surfaces: string | string[] | null | undefined) => InkPolarity; /** Every evaluable paint in a slide fill (gradient stops, else the flat surface). */ export declare const resolveSlideSurfaceColors: (background: SlideBackground | undefined, fallback?: string) => string[]; /** Surfaces the element's text actually sits on: opaque own fill, else the slide. */ export declare const resolveElementSurfaces: (options?: { fill?: string; background?: SlideBackground; fallbackSurface?: string; }) => string[]; /** * Default body ink for new / unstyled text: strictly black or white from * {@link preferredInk}. Explicit saturated colors pass through. */ export declare const resolveDefaultFontColor: (color: string, surface: string | string[] | null | undefined) => string; /** Default font color against a slide background (and optional own fill). */ export declare const resolveElementDefaultFontColor: (preferred: string, options?: { fill?: string; background?: SlideBackground; fallbackSurface?: string; }) => string; export type LiveTextPaintOptions = { fill?: string; background?: SlideBackground; fallbackSurface?: string; themeFontColor?: string; /** The text/shape being painted. Own fill/gradient win; otherwise under-paints. */ element?: PPTTextElement | PPTShapeElement; /** Painter-order siblings so overlay labels see the chip/card underneath. */ elements?: PPTElement[]; }; /** * Surfaces the text actually sits on — same query as import contrast. * Own opaque fill/gradient first; otherwise the topmost paint under the box. */ export declare const resolveTextPaintSurfaces: (el: PPTTextElement | PPTShapeElement | undefined, options?: LiveTextPaintOptions) => string[]; /** * Single paint-time contrast pass for the live editor and the preview raster. * Surfaces match {@link fixSlideTextContrast}: overlay labels on a chip use * the chip, not the slide paper. Default inks rewrite to the same hex. */ export declare const resolveLiveTextPaint: (preferred: string | undefined, html: string, options?: LiveTextPaintOptions) => { ink: string; html: string; }; /** * Chart axis / legend / label ink. Same rules as text: default greys / black / * white snap to {@link preferredInk} on the chart fill, else the slide. * Explicit saturated colors pass through. */ export declare const resolveChartLabelColor: (chart: { textColor?: string; fill?: string; }, options?: { background?: SlideBackground; fallbackSurface?: string; fontColor?: string; }) => string; /** Lift only series that disappear into the slide (Ink #171717 is ~1.1:1). */ export declare const CHART_SERIES_CONTRAST = 2; /** * Series fills/strokes must clear the slide (or chart fill). Ink's #171717 * on #0c0d10 is ~1:1 — lift it instead of painting invisible bars/lines. */ export declare const resolveChartSeriesColors: (colors: string[] | undefined, surfaces: string | string[] | null | undefined) => string[]; export declare const resolveChartElementSeriesColors: (chart: { themeColors?: string[]; fill?: string; }, options?: { background?: SlideBackground; fallbackSurface?: string; }) => string[]; /** * Placeholder ink is the same binary polarity as body text — never a mixed grey. */ export declare const resolvePlaceholderColor: (options: { author?: string; body?: string; surfaces: string[]; }) => InkPolarity; /** Distinct normalized text colors declared in an HTML content string. */ export declare const collectHtmlTextColors: (html: string) => string[]; /** Rewrite `color:` declarations via `replace` (return null to keep as-is). */ export declare const rewriteHtmlTextColors: (html: string, replace: (color: string) => string | null) => string; export type ContrastAction = { kind: 'fix'; slideIndex: number; elementIndex: number; elementType: 'text' | 'shape' | 'latex' | 'table'; target: 'html' | 'defaultColor' | 'latex' | 'tableCell'; from: string; to: string; backgrounds: string[]; ratioBefore: number; ratioAfter: number; snippet?: string; } | { kind: 'skip'; slideIndex: number; elementIndex: number; elementType: 'text' | 'shape' | 'latex' | 'table'; reason: string; colors?: string[]; backgrounds?: string[]; snippet?: string; }; /** * Same decisions as {@link fixSlideTextContrast}, but returns a per-target * action log (fixes + skips) instead of mutating. Used for verification. */ export declare const diagnoseSlideTextContrast: (slide: Slide, theme: ThemeColors, slideIndex?: number, ctx?: ContrastContext) => ContrastAction[]; /** * Repair unreadable text colors on a slide in place. Returns the number of * applied fixes. Used for AI-generated imports only — user background changes * go through {@link applySlideBackgroundWithContrast} / {@link preferredInk}. */ export declare const fixSlideTextContrast: (slide: Slide, theme: ThemeColors, ctx?: ContrastContext) => number; export declare const rewriteDefaultInksInHtml: (html: string, ink: string) => string; /** * Clone a slide and snap *default* ink (theme grey / black / white) to * {@link preferredInk} for the new slide fill. Explicit colors and text on * opaque own fills are left alone. Mutates nothing on the input. */ export declare const applySlideBackgroundWithContrast: (slide: Slide, theme: ThemeColors) => Slide; export {};