A default export is 2 pixels per point. Without a resolution, Preview
treats those pixels as 72 dpi and the picture opens at twice the slide's
size. PNG pHYs and JPEG JFIF density now say scale times 72, so the
default is 144 dpi. Screenshots stay untagged.
Co-authored-by: Cursor <cursoragent@cursor.com>
DrawCurve, DrawClosedCurve and FillClosedCurve (GDI+ cardinal splines, the
smoothed lines of charts and freehand shapes) draw as cubic Béziers with
the record's tension, offset and segment count; FillRegion fills the region
object's area. These records were skipped, so the shapes were missing.
Co-authored-by: Cursor <cursoragent@cursor.com>
The preedit was already stored in the cell. It is now laid out the same
way the cell is drawn, and the editor underlines it in place, thicker
under the part being converted. The caret and the input method's
candidate window sit in that cell. Notes stay an egui text field.
Co-authored-by: Cursor <cursoragent@cursor.com>
A pen reading of 1 is the full pen width. A lighter touch draws a thinner
stroke, and a zero reading stays visible at 8% of the pen. A mouse, a
missing or non-finite reading, and a stroke whose samples are all the old
placeholder 0.5 still draw at the pen width. PPTX keeps one width per
stroke: the mean of the samples for a pressured stroke, the pen width for
a legacy one. Tilt is not read.
Co-authored-by: Cursor <cursoragent@cursor.com>
Radar and filled radar charts are drawn polar: spokes per category,
polygon gridlines and value labels up the first spoke. Stock charts
draw high-low lines with close dashes, or up/down bars between open
and close, and are saved as c:stockChart (hiLowLines, upDownBars)
instead of a line chart; c:upDownBars is read, and chart.options takes
upDownBars. New stock charts start as high-low-close and new combo
charts as columns with a line.
Co-authored-by: Cursor <cursoragent@cursor.com>
Read the regionMap chartEx layout as a RegionMap chart (regions and
their colour values), write the part back as read while unchanged, and
when edited rebuild it with the original geography (projection, culture
and cached boundaries) carried over. With no map data in the repo, the
renderer draws each region as a value-coloured tile with a colour scale
and a note that the map itself is not drawn.
Co-authored-by: Cursor <cursoragent@cursor.com>
sp3d extrusionH (with extrusionClr, srgb or scheme) under a camera that
turns the shape now draws the shape's sides: its outline swept from the
back face to the front along the camera's view of the depth axis, in the
side colour shaded, before the face. camera::project3 projects points
off the shape's plane.
Read, keep and write every cx:lvl of a chartEx category dimension,
build a category tree from runs of parent labels, and draw treemaps as
nested squarified groups with banners and sunbursts as one ring per
level. Box & whisker honours quartileMethod="inclusive" (QUARTILE.INC)
on import, render, save and the chart.options command.
Co-authored-by: Cursor <cursoragent@cursor.com>
Histograms and Paretos bin their observations (bin count, bin width or
Scott's rule) and draw as touching columns, a Pareto with its cumulative
line on a percentage axis. Box and whisker charts draw exclusive-quartile
boxes, 1.5 IQR whiskers, means and outliers per category group.
Edited and new Office 2016 charts are written as chartEx parts from the
model (with the plain-chart fallback), so a waterfall stays a waterfall.
The chart menus list all seven types, and chart.options sets bins and
waterfall totals.
Co-authored-by: Cursor <cursoragent@cursor.com>
Notes pages and handouts lay out the master's header, date, footer and
page-number placeholders with deckcraft-text (render::place_master_texts),
so their text wraps in the placeholder and keeps mixed run formatting,
drawn as visible glyphs. Stacked on #580.
An owned corpus of those element kinds survives open, save and reopen.
Paragraph default run properties, including a link, and action sounds
were dropped on save, so import and export now keep them.
These Office 2016 charts were drawn as plain columns. Now:
- waterfall: bars float from the running total, total points
(cx:subtotals, now read into Chart.subtotals) stand on the axis and
reset it, increases/decreases/totals take accents 1-3 with a matching
legend, and connectors join each bar to the next;
- funnel: centred bars as wide as their value, category labels on the
left and values inside;
- treemap: squarified tiles in proportion to their value, labelled;
- sunburst: the innermost level as a ring with an open centre.
Co-authored-by: Cursor <cursoragent@cursor.com>
Insert › Links gains Zoom, with PowerPoint's three kinds: Slide Zoom
(pictures of chosen slides on the current slide), Section Zoom (one per
chosen section) and Summary Zoom (a new Title Only slide in front of
the chosen parts). Each zoom is a picture of its target that jumps to
it in the show. With Return to Zoom, on by default for section and
summary zooms as in PowerPoint, the show comes back to the zoom slide,
fully built, after the target slide or the section's last slide; going
elsewhere (a slide number, See All Slides) forgets it. Hyperlink gains
zoom_return for this; .pptx saves zooms as the linked pictures
PowerPoint writes as their fallback, so the return flag is DeckCraft-
only for now. Live thumbnails, the zoom-in animation and the native
p166:sldZm element are still missing; gaps.md says so.
Co-authored-by: Cursor <cursoragent@cursor.com>
ui_soak_tests.rs drives the app through the egui test harness with seeded
random clicks, drags, keys (plain, Ctrl, Shift), typing and scrolling over the
sample deck. Every ten actions it checks that every document validates, the
selection is in range, undo, journal, thumbnail and texture caches stay
bounded, and no command or render panic was caught; at the end, that input
really reached the editor and the mean frame stays under 250 ms. Two seeds x
150 actions run in CI; DECKCRAFT_UI_SOAK_STEPS/SEED run it long.
Panics swallowed by the guards were invisible to tests: Session now counts
them (panics_caught) and deckcraft_render::panics_caught() counts render
panics drawn as blank. Textures::thumb_count exposes the thumbnail cache size.
Co-authored-by: Cursor <cursoragent@cursor.com>
soak_tests.rs drives every non-file command with plausible and hostile
parameters over a ~100-slide deck, checking after each step that the
document validates, the selection is in range, the undo stack is bounded,
undo then redo restores the exact document, and every 100 steps that the
deck saves and reopens as .deckcraft and .pptx. Four seeds x 400 steps run
in CI (under a second); DECKCRAFT_SOAK_STEPS/SEED run it long.
The long soak found that a chart a few points wide panicked in f64::clamp
(label gutter min above max), and the render guard then blanked the whole
slide. chart.rs now uses a non-panicking clamp_soft for the gutters and the
zero line; a render test draws tiny charts next to a shape without the
guard.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pie and doughnut data labels still went through fmt_num, which rounded
anything non-integral to one decimal (999.99 drew as "1000.0"). They now
use Chart::data_label like the other label paths (series format, else
axis format, else General), and fmt_num is gone.
Co-authored-by: Cursor <cursoragent@cursor.com>
decode_at accepted up to 400 million pixels and then called to_rgba8, which
the decoder's allocation limit doesn't cover (a grey image converts to four
times its decoded size), and every picture that failed to decode was kept,
bytes and all, for the life of the process.
- Raster decodes check the RGBA size (512 MiB cap) from the header before
decoding and again before the conversion.
- The failed-source cache keeps at most 256 entries and 64 MiB of encoded
bytes, oldest forgotten first; a larger failed input isn't kept, and a hit
must be the same Arc.
Co-authored-by: Cursor <cursoragent@cursor.com>
A shape path gradient tested every raster pixel against every flattened
outline edge, so an outline with tens of thousands of segments cost
billions of tests and stalled rendering. The outline is now simplified
before rasterising so pixels x edges stays under 64M: points closer than
a raster pixel are dropped first, then every k-th point is kept, with
each polygon left closed and at least 8 edges.
Co-authored-by: Cursor <cursoragent@cursor.com>
alloc_id bumped next_id with saturating_add: a presentation whose
next_id reached u32::MAX (a hostile file can arrive that way;
fix_next_id imports it) handed out that same id to every new slide,
shape, layout and media - duplicate ids, ambiguous references, silent
corruption on every later insert.
alloc_id now returns Option<u32> via checked_add, and every caller
refuses: the engine's insert/duplicate/media/layout commands answer
with a clear error naming the command, paste and duplicate re-id
helpers propagate it, media addition is fallible, and a fresh slide is
Option. The pptx export's synthesized blank layout is guarded the same
way. Loading keeps such a deck viewable: repair re-ids duplicate
slides while it can, and a duplicate it cannot re-id stays as it is -
growth is refused, and only that duplicate keeps its ambiguous id.
Renumbering the loaded graph was rejected: it needs the
cross-document reference remapping that issue #406 tracks.
Tests: model exhaustion (alloc_id None, fresh media refused, a new
slide refused), engine (slide.new, shape.insert, slide.duplicate and
edit.duplicate all refuse; the document holds no duplicate ids),
format repair (an unre-id-able duplicate stays visible).
INTERSECTCLIPRECT, EXCLUDECLIPRECT and EXTSELECTCLIPRGN limit what later records paint, including a region made of several rectangles. A rotated map stores the bounds of the clip rectangle.
Co-authored-by: Cursor <cursoragent@cursor.com>
A read-only command, {slide?, x, y (slide units), scale?}: the slide is rendered and
the pixel at the point is returned as {color: "RRGGBB", alpha, x, y}. This is what the
Eyedropper takes, and what scripts and MCP use to read a colour off a slide. A point
outside the slide, a missing or non-numeric coordinate or an unknown slide is an error.
A shape with scene3d and sp3d but no bevelT was drawn in its flat fill
colour. PowerPoint lights the front face from the light rig whether or
not there is a bevel, so a plain 3-D rotated rectangle is lighter than
its fill. Model the face with the same threePt approximation as the
bevel (no rim), for solid fills.
Wild 3-D slides: s8 6.35 -> 4.26, s9 19.74 -> 17.68. Corpus unchanged
except lo tdf114848 s4 2.13 -> 1.94.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shape seen through a 3-D camera preset (isometric, perspective...) or
turned with a:camera/a:rot drew flat and front-on. PowerPoint turns the
shape's plane in space and projects it. Fitted black-box to its exports
(a picture with coloured corner markers under every preset and several
rotations; every fit within half a point on a 100 x 75 pt shape): points
about the centre are turned Rz(-rev) Rx(lat) Ry(lon), perspective cameras
then dividing by D / (D - z) with D about 5-16 shape sizes; a:rot turns
the other way about the horizontal and vertical axes. DeckCraft now draws
the shape under the affine transform that best fits its turned corners:
exact for orthographic cameras, within 2% of the size for perspective
ones (an affine map can't foreshorten). Oblique cameras leave a flat
shape as it is; depth (extrusion) is not drawn.
Wild synthetic set: camera presets 15.02 -> 6.35, 3-D picture styles
16.00 -> 7.72, extruded cameras 21.41 -> 19.74.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A reflection on a picture (PowerPoint's "reflected" picture styles) drew
nothing: the reflection painted the shape's fill, and a picture has none.
It now mirrors the picture itself, clipped to its outline, under the same
fade.
Wild synthetic set: picture effects slide 16.77 -> 16.00 (the rest is
3-D camera rotation, not modelled).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shape's picture fill marked not to rotate with the shape turned and
flipped with it (rotWithShape was neither kept nor written: always 1).
PowerPoint keeps such a picture upright, stretched (or tiled) over the
box the rotated shape covers on the slide and clipped to the shape. Now
read, written (PictureFill::upright) and drawn so.
Wild synthetic set: picture fills on rotated/flipped shapes 12.89 ->
3.87.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An outline naming no join was always rounded. PowerPoint rounds most
presets but mitres some by default: classified per preset by comparing
its export (8 pt outline, no join) with round- and miter-joined renders:
the plus, right/up/down block arrows (not the left one), chevrons, stars
from five points up, the equation shapes, curved ribbons and the action
buttons. Miter joins also take PowerPoint's default limit of 8 widths
(kurbo's default was 4), so sharp star points no longer bevel off.
Wild synthetic set: joins slide 0.43 -> 0.21, outline-on-pattern slide
3.95 -> 3.76, preset galleries slightly better.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A gradient fill marked not to rotate with its shape turned and flipped
with it anyway. PowerPoint lays such a gradient out upright, over the box
the rotated or flipped shape covers on the slide (a 90 degree gradient on
a quarter-turned triangle still runs top to bottom). Linear and radial
gradients now do; rect and outline path gradients still follow the shape.
Wild synthetic set: gradients on rotated/flipped shapes 5.48 -> 1.34,
gradient angles slide 6.75 -> 1.95.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shadow of a picture is the picture recoloured to the shadow colour,
but drawing the picture left its pixel-space transform set, so the
recolouring rectangle (sized in points) covered only part of a picture
whose bitmap is bigger than it is shown: a 400 px photo at 240 pt cast a
shadow of its top-left corner only. The shape's transform is restored
before recolouring.
Wild synthetic set: framed pictures with shadows 7.67 -> 5.95, rotated
picture with a shadow 10.67 -> 10.07.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Explicit cell borders were stroked as plain solid lines (no dash, compound
or cap), and diagonal borders (lnTlToBr, lnBlToTr) were read but never
drawn. Both now go through the shape line stroker; diagonals are drawn
under the cell edges.
Wild synthetic set: border styles slide 5.67 -> 2.26, thick merged-cell
borders 1.20 -> 0.69.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shape with a top bevel seen through a 3-D scene drew flat. Fitted to
PowerPoint's export of the bevel presets on a solid fill with the default
three-point light rig and matte material, DeckCraft now lights the face
and a band as wide as the bevel inside the outline as surfaces: Lambert
light from the upper left (azimuth 240, 45 degrees up), c * (0.453 +
0.731 * n.L) + 14.7 per channel, the band's normal tilting outwards by
the profile's slope (quarter circle by default, straight for angle).
Within about 15 grey levels RMS of PowerPoint on the circle bevel; other
profiles use the circle's, pictures get light and dark veils, and other
light rigs and materials (and anything without a scene) stay flat.
Wild synthetic set: bevel presets slide 5.20 -> 2.48, materials/rigs
13.25 -> 12.73.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A "shape" path gradient with its focus off the shape (fillToRect on a
corner of an ellipse's box) filled solid with its last colour: the
position came from the nearest crossing of the ray from the focus with
the outline, and rays from outside cross it twice or miss. The contours
are the outline scaled about the focus, which a point first reaches at
the ray's farthest crossing; that is now used. A focus rectangle (rather
than a point) is the first colour throughout, with the colour running from
its edge to the outline, as PowerPoint draws it.
Wild synthetic set: shape path gradients 18.95 -> 3.54.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- a:biLevel (Color > Black and White) was ignored. It is now read, written
and drawn: white where the luminance reaches the threshold, else black
(observed thresholds at 30/50/70% on colour swatches).
- a:alphaInv was ignored; an opaque picture with it is invisible in
PowerPoint. Now read, written and drawn.
- Grayscale, duotone and saturation weighed channels 0.299/0.587/0.114;
PowerPoint's export greys pure red to 54 and green to 182: Rec. 709
weights (0.2126/0.7152/0.0722).
Wild synthetic set: recolour slide 7.60 -> 1.15, picture corrections
13.36 -> 7.36.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brightness was added after the contrast stretch. Measured on PowerPoint's
export of grey ramps (5 x 5 grid of -80..80% brightness and contrast):
contrast scales about mid-grey (x(1+c) below zero, /(1-c) above) and
brightness adds b * 255 scaled by (1 + cf) / 2, half before the stretch
and half after; +40% brightness with +40% contrast maps black to 54, not
17. DeckCraft now matches the 25 ramps within 5 grey levels.
Wild synthetic set: brightness/contrast grid 15.93 -> 13.36, recolour
slide 8.74 -> 7.60.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The decoded-picture cache keyed adjustments with an xor-multiply mix in
which brightness b, contrast c and -b, -c collide (the sign bits cancel),
so of two copies of a picture, one +40% bright and contrast and one -40%,
both showed whichever was decoded first. The key is now a real hash.
Wild synthetic set: brightness/contrast grid 27.86 -> 15.93.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>