summaryrefslogtreecommitdiff
path: root/tests/suite/visualize
diff options
context:
space:
mode:
Diffstat (limited to 'tests/suite/visualize')
-rw-r--r--tests/suite/visualize/circle.typ69
-rw-r--r--tests/suite/visualize/color.typ331
-rw-r--r--tests/suite/visualize/ellipse.typ31
-rw-r--r--tests/suite/visualize/gradient.typ631
-rw-r--r--tests/suite/visualize/image.typ122
-rw-r--r--tests/suite/visualize/line.typ92
-rw-r--r--tests/suite/visualize/path.typ52
-rw-r--r--tests/suite/visualize/pattern.typ131
-rw-r--r--tests/suite/visualize/polygon.typ51
-rw-r--r--tests/suite/visualize/rect.typ107
-rw-r--r--tests/suite/visualize/square.typ146
-rw-r--r--tests/suite/visualize/stroke.typ171
12 files changed, 1934 insertions, 0 deletions
diff --git a/tests/suite/visualize/circle.typ b/tests/suite/visualize/circle.typ
new file mode 100644
index 00000000..43459eb5
--- /dev/null
+++ b/tests/suite/visualize/circle.typ
@@ -0,0 +1,69 @@
+// Test the `circle` function.
+
+--- circle ---
+// Default circle.
+#box(circle())
+#box(circle[Hey])
+
+--- circle-auto-sizing ---
+// Test auto sizing.
+#set circle(inset: 0pt)
+
+Auto-sized circle.
+#circle(fill: rgb("eb5278"), stroke: 2pt + black,
+ align(center + horizon)[But, soft!]
+)
+
+Center-aligned rect in auto-sized circle.
+#circle(fill: forest, stroke: conifer,
+ align(center + horizon,
+ rect(fill: conifer, inset: 5pt)[But, soft!]
+ )
+)
+
+Rect in auto-sized circle.
+#circle(fill: forest,
+ rect(fill: conifer, stroke: white, inset: 4pt)[
+ #set text(8pt)
+ But, soft! what light through yonder window breaks?
+ ]
+)
+
+Expanded by height.
+#circle(stroke: black, align(center)[A \ B \ C])
+
+--- circle-directly-in-rect ---
+// Ensure circle directly in rect works.
+#rect(width: 40pt, height: 30pt, fill: forest,
+ circle(fill: conifer))
+
+--- circle-relative-sizing ---
+// Test relative sizing.
+#set text(fill: white)
+#show: rect.with(width: 100pt, height: 50pt, inset: 0pt, fill: rgb("aaa"))
+#set align(center + horizon)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ 1fr,
+ circle(radius: 10pt, fill: eastern, [A]), // D=20pt
+ circle(height: 60%, fill: eastern, [B]), // D=30pt
+ circle(width: 20% + 20pt, fill: eastern, [C]), // D=40pt
+ 1fr,
+)
+
+--- circle-radius-width-and-height ---
+// Radius wins over width and height.
+// Error: 23-34 unexpected argument: width
+#circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern)
+
+--- circle-sizing-options ---
+// Test different ways of sizing.
+#set page(width: 120pt, height: 40pt)
+#stack(
+ dir: ltr,
+ spacing: 2pt,
+ circle(radius: 5pt),
+ circle(width: 10%),
+ circle(height: 50%),
+)
diff --git a/tests/suite/visualize/color.typ b/tests/suite/visualize/color.typ
new file mode 100644
index 00000000..6cf887a4
--- /dev/null
+++ b/tests/suite/visualize/color.typ
@@ -0,0 +1,331 @@
+// Test color modification methods.
+
+--- color-mix ---
+// Compare both ways.
+#test-repr(rgb(0%, 30.2%, 70.2%), rgb("004db3"))
+
+// Alpha channel.
+#test(rgb(255, 0, 0, 50%), rgb("ff000080"))
+
+// Test color modification methods.
+#test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66))
+#test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18))
+#test(rgb("#133337").negate(space: rgb), rgb(236, 204, 200))
+#test(white.lighten(100%), white)
+
+// Color mixing, in Oklab space by default.
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"))), rgb("#d0a800"))
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: oklab)), rgb("#d0a800"))
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: rgb)), rgb("#808000"))
+
+#test(rgb(color.mix(red, green, blue)), rgb("#909282"))
+#test(rgb(color.mix(red, blue, green)), rgb("#909282"))
+#test(rgb(color.mix(blue, red, green)), rgb("#909282"))
+
+// Mix with weights.
+#test(rgb(color.mix((red, 50%), (green, 50%))), rgb("#c0983b"))
+#test(rgb(color.mix((red, 0.5), (green, 0.5))), rgb("#c0983b"))
+#test(rgb(color.mix((red, 5), (green, 5))), rgb("#c0983b"))
+#test(rgb(color.mix((green, 5), (white, 0), (red, 5))), rgb("#c0983b"))
+#test(color.mix((rgb("#aaff00"), 25%), (rgb("#aa00ff"), 75%), space: rgb), rgb("#aa40bf"))
+#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: rgb), rgb("#aa8080"))
+#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: rgb), rgb("#aabf40"))
+
+// Mix in hue-based space.
+#test(rgb(color.mix(red, blue, space: color.hsl)), rgb("#c408ff"))
+#test(rgb(color.mix((red, 50%), (blue, 100%), space: color.hsl)), rgb("#5100f8"))
+// Error: 6-51 cannot mix more than two colors in a hue-based space
+#rgb(color.mix(red, blue, white, space: color.hsl))
+
+--- color-conversion ---
+// Test color conversion method kinds
+#test(rgb(rgb(10, 20, 30)).space(), rgb)
+#test(color.linear-rgb(rgb(10, 20, 30)).space(), color.linear-rgb)
+#test(oklab(rgb(10, 20, 30)).space(), oklab)
+#test(oklch(rgb(10, 20, 30)).space(), oklch)
+#test(color.hsl(rgb(10, 20, 30)).space(), color.hsl)
+#test(color.hsv(rgb(10, 20, 30)).space(), color.hsv)
+#test(cmyk(rgb(10, 20, 30)).space(), cmyk)
+#test(luma(rgb(10, 20, 30)).space(), luma)
+
+#test(rgb(color.linear-rgb(10, 20, 30)).space(), rgb)
+#test(color.linear-rgb(color.linear-rgb(10, 20, 30)).space(), color.linear-rgb)
+#test(oklab(color.linear-rgb(10, 20, 30)).space(), oklab)
+#test(oklch(color.linear-rgb(10, 20, 30)).space(), oklch)
+#test(color.hsl(color.linear-rgb(10, 20, 30)).space(), color.hsl)
+#test(color.hsv(color.linear-rgb(10, 20, 30)).space(), color.hsv)
+#test(cmyk(color.linear-rgb(10, 20, 30)).space(), cmyk)
+#test(luma(color.linear-rgb(10, 20, 30)).space(), luma)
+
+#test(rgb(oklab(10%, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(oklab(10%, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(oklab(10%, 20%, 30%)).space(), oklab)
+#test(oklch(oklab(10%, 20%, 30%)).space(), oklch)
+#test(color.hsl(oklab(10%, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(oklab(10%, 20%, 30%)).space(), color.hsv)
+#test(cmyk(oklab(10%, 20%, 30%)).space(), cmyk)
+#test(luma(oklab(10%, 20%, 30%)).space(), luma)
+
+#test(rgb(oklch(60%, 40%, 0deg)).space(), rgb)
+#test(color.linear-rgb(oklch(60%, 40%, 0deg)).space(), color.linear-rgb)
+#test(oklab(oklch(60%, 40%, 0deg)).space(), oklab)
+#test(oklch(oklch(60%, 40%, 0deg)).space(), oklch)
+#test(color.hsl(oklch(60%, 40%, 0deg)).space(), color.hsl)
+#test(color.hsv(oklch(60%, 40%, 0deg)).space(), color.hsv)
+#test(cmyk(oklch(60%, 40%, 0deg)).space(), cmyk)
+#test(luma(oklch(60%, 40%, 0deg)).space(), luma)
+
+#test(rgb(color.hsl(10deg, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(color.hsl(10deg, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(color.hsl(10deg, 20%, 30%)).space(), oklab)
+#test(oklch(color.hsl(10deg, 20%, 30%)).space(), oklch)
+#test(color.hsl(color.hsl(10deg, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(color.hsl(10deg, 20%, 30%)).space(), color.hsv)
+#test(cmyk(color.hsl(10deg, 20%, 30%)).space(), cmyk)
+#test(luma(color.hsl(10deg, 20%, 30%)).space(), luma)
+
+#test(rgb(color.hsv(10deg, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(color.hsv(10deg, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(color.hsv(10deg, 20%, 30%)).space(), oklab)
+#test(oklch(color.hsv(10deg, 20%, 30%)).space(), oklch)
+#test(color.hsl(color.hsv(10deg, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(color.hsv(10deg, 20%, 30%)).space(), color.hsv)
+#test(cmyk(color.hsv(10deg, 20%, 30%)).space(), cmyk)
+#test(luma(color.hsv(10deg, 20%, 30%)).space(), luma)
+
+#test(rgb(cmyk(10%, 20%, 30%, 40%)).space(), rgb)
+#test(color.linear-rgb(cmyk(10%, 20%, 30%, 40%)).space(), color.linear-rgb)
+#test(oklab(cmyk(10%, 20%, 30%, 40%)).space(), oklab)
+#test(oklch(cmyk(10%, 20%, 30%, 40%)).space(), oklch)
+#test(color.hsl(cmyk(10%, 20%, 30%, 40%)).space(), color.hsl)
+#test(color.hsv(cmyk(10%, 20%, 30%, 40%)).space(), color.hsv)
+#test(cmyk(cmyk(10%, 20%, 30%, 40%)).space(), cmyk)
+#test(luma(cmyk(10%, 20%, 30%, 40%)).space(), luma)
+
+#test(rgb(luma(10%)).space(), rgb)
+#test(color.linear-rgb(luma(10%)).space(), color.linear-rgb)
+#test(oklab(luma(10%)).space(), oklab)
+#test(oklch(luma(10%)).space(), oklch)
+#test(color.hsl(luma(10%)).space(), color.hsl)
+#test(color.hsv(luma(10%)).space(), color.hsv)
+#test(cmyk(luma(10%)).space(), cmyk)
+#test(luma(luma(10%)).space(), luma)
+
+#test(rgb(1, 2, 3).to-hex(), "#010203")
+#test(rgb(1, 2, 3, 4).to-hex(), "#01020304")
+#test(luma(40).to-hex(), "#282828")
+#test-repr(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e0dcda")
+#test-repr(rgb(cmyk(4%, 5%, 6%, 7%)), rgb(87.84%, 86.27%, 85.49%, 100%))
+#test-repr(rgb(luma(40%)), rgb(40%, 40%, 40%))
+#test-repr(cmyk(luma(40)), cmyk(11.76%, 10.67%, 10.51%, 14.12%))
+#test-repr(cmyk(rgb(1, 2, 3)), cmyk(66.67%, 33.33%, 0%, 98.82%))
+#test-repr(luma(rgb(1, 2, 3)), luma(0.73%))
+#test-repr(color.hsl(luma(40)), color.hsl(0deg, 0%, 15.69%))
+#test-repr(color.hsv(luma(40)), color.hsv(0deg, 0%, 15.69%))
+#test-repr(color.linear-rgb(luma(40)), color.linear-rgb(2.12%, 2.12%, 2.12%))
+#test-repr(color.linear-rgb(rgb(1, 2, 3)), color.linear-rgb(0.03%, 0.06%, 0.09%))
+#test-repr(color.hsl(rgb(1, 2, 3)), color.hsl(-150deg, 50%, 0.78%))
+#test-repr(color.hsv(rgb(1, 2, 3)), color.hsv(-150deg, 66.67%, 1.18%))
+#test-repr(oklab(luma(40)), oklab(27.68%, 0.0, 0.0, 100%))
+#test-repr(oklab(rgb(1, 2, 3)), oklab(8.23%, -0.004, -0.007, 100%))
+#test-repr(oklch(oklab(40%, 0.2, 0.2)), oklch(40%, 0.283, 45deg, 100%))
+#test-repr(oklch(luma(40)), oklch(27.68%, 0.0, 72.49deg, 100%))
+#test-repr(oklch(rgb(1, 2, 3)), oklch(8.23%, 0.008, 240.75deg, 100%))
+
+--- color-spaces ---
+// The the different color spaces
+#let col = rgb(50%, 64%, 16%)
+#box(square(size: 9pt, fill: col))
+#box(square(size: 9pt, fill: rgb(col)))
+#box(square(size: 9pt, fill: oklab(col)))
+#box(square(size: 9pt, fill: oklch(col)))
+#box(square(size: 9pt, fill: luma(col)))
+#box(square(size: 9pt, fill: cmyk(col)))
+#box(square(size: 9pt, fill: color.linear-rgb(col)))
+#box(square(size: 9pt, fill: color.hsl(col)))
+#box(square(size: 9pt, fill: color.hsv(col)))
+
+--- color-space ---
+// Test color kind method.
+#test(rgb(1, 2, 3, 4).space(), rgb)
+#test(cmyk(4%, 5%, 6%, 7%).space(), cmyk)
+#test(luma(40).space(), luma)
+#test(rgb(1, 2, 3, 4).space() != luma, true)
+
+--- color-components ---
+// Test color '.components()' without conversions
+
+#let test-components(col, ref, has-alpha: true) = {
+ // Perform an approximate scalar comparison.
+ let are-equal((a, b)) = {
+ let to-float(x) = if type(x) == angle { x.rad() } else { float(x) }
+ let epsilon = 1e-4 // The maximum error between both numbers
+ assert.eq(type(a), type(b))
+ calc.abs(to-float(a) - to-float(b)) < epsilon
+ }
+
+ let ref-without-alpha = if has-alpha { ref.slice(0, -1) } else { ref }
+ assert.eq(col.components().len(), ref.len())
+ assert(col.components().zip(ref).all(are-equal))
+ assert(col.components(alpha: false).zip(ref-without-alpha).all(are-equal))
+}
+#test-components(rgb(1, 2, 3, 4), (0.39%, 0.78%, 1.18%, 1.57%))
+#test-components(luma(40), (15.69%, 100%))
+#test-components(luma(40, 50%), (15.69%, 50%))
+#test-components(cmyk(4%, 5%, 6%, 7%), (4%, 5%, 6%, 7%), has-alpha: false)
+#test-components(oklab(10%, 0.2, 0.4), (10%, 0.2, 0.4, 100%))
+#test-components(oklch(10%, 0.2, 90deg), (10%, 0.2, 90deg, 100%))
+#test-components(oklab(10%, 50%, 200%), (10%, 0.2, 0.8, 100%))
+#test-components(oklch(10%, 50%, 90deg), (10%, 0.2, 90deg, 100%))
+#test-components(color.linear-rgb(10%, 20%, 30%), (10%, 20%, 30%, 100%))
+#test-components(color.hsv(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
+#test-components(color.hsl(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
+
+--- color-luma ---
+// Test gray color conversion.
+#stack(dir: ltr, rect(fill: luma(0)), rect(fill: luma(80%)))
+
+--- color-rgb-out-of-range ---
+// Error for values that are out of range.
+// Error: 11-14 number must be between 0 and 255
+#test(rgb(-30, 15, 50))
+
+--- color-rgb-bad-string ---
+// Error: 6-11 color string contains non-hexadecimal letters
+#rgb("lol")
+
+--- color-rgb-missing-argument-red ---
+// Error: 2-7 missing argument: red component
+#rgb()
+
+--- color-rgb-missing-argument-blue ---
+// Error: 2-11 missing argument: blue component
+#rgb(0, 1)
+
+--- color-rgb-bad-type ---
+// Error: 21-26 expected integer or ratio, found boolean
+#rgb(10%, 20%, 30%, false)
+
+--- color-luma-unexpected-argument ---
+// Error: 10-20 unexpected argument: key
+#luma(1, key: "val")
+
+--- color-mix-bad-amount-type ---
+// Error: 12-24 expected float or ratio, found string
+// Error: 26-39 expected float or ratio, found string
+#color.mix((red, "yes"), (green, "no"), (green, 10%))
+
+--- color-mix-bad-value ---
+// Error: 12-23 expected a color or color-weight pair
+#color.mix((red, 1, 2))
+
+--- color-mix-bad-space-type ---
+// Error: 31-38 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`, found string
+#color.mix(red, green, space: "cyber")
+
+--- color-mix-bad-space-value-1 ---
+// Error: 31-36 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`
+#color.mix(red, green, space: image)
+
+--- color-mix-bad-space-value-2 ---
+// Error: 31-41 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`
+#color.mix(red, green, space: calc.round)
+
+--- color-cmyk-ops ---
+// Test CMYK color conversion.
+#let c = cmyk(50%, 64%, 16%, 17%)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)),
+ rect(width: 1cm, fill: c),
+ rect(width: 1cm, fill: c.negate(space: cmyk)),
+)
+
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: c.lighten(x * 10%)))
+}
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: c.darken(x * 10%)))
+}
+
+--- color-outside-srgb-gamut ---
+// Colors outside the sRGB gamut.
+#box(square(size: 9pt, fill: oklab(90%, -0.2, -0.1)))
+#box(square(size: 9pt, fill: oklch(50%, 0.5, 0deg)))
+
+--- color-rotate-hue ---
+// Test hue rotation
+#let col = rgb(50%, 64%, 16%)
+
+// Oklch
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg)))
+}
+
+// HSL
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsl)))
+}
+
+// HSV
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsv)))
+}
+
+--- color-saturation ---
+// Test saturation
+#let col = color.hsl(180deg, 0%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.saturate(x * 10%)))
+}
+
+#let col = color.hsl(180deg, 100%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.desaturate(x * 10%)))
+}
+
+#let col = color.hsv(180deg, 0%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.saturate(x * 10%)))
+}
+
+#let col = color.hsv(180deg, 100%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.desaturate(x * 10%)))
+}
+
+--- color-luma-ops ---
+// Test gray color modification.
+#test-repr(luma(20%).lighten(50%), luma(60%))
+#test-repr(luma(80%).darken(20%), luma(64%))
+#test-repr(luma(80%).negate(space: luma), luma(20%))
+
+--- color-transparentize ---
+// Test alpha modification.
+#test-repr(luma(100%, 100%).transparentize(50%), luma(100%, 50%))
+#test-repr(luma(100%, 100%).transparentize(75%), luma(100%, 25%))
+#test-repr(luma(100%, 50%).transparentize(50%), luma(100%, 25%))
+#test-repr(luma(100%, 10%).transparentize(250%), luma(100%, 0%))
+#test-repr(luma(100%, 40%).transparentize(-50%), luma(100%, 70%))
+#test-repr(luma(100%, 0%).transparentize(-100%), luma(100%, 100%))
+
+--- color-opacify ---
+#test-repr(luma(100%, 50%).opacify(50%), luma(100%, 75%))
+#test-repr(luma(100%, 20%).opacify(100%), luma(100%, 100%))
+#test-repr(luma(100%, 100%).opacify(250%), luma(100%, 100%))
+#test-repr(luma(100%, 50%).opacify(-50%), luma(100%, 25%))
+#test-repr(luma(100%, 0%).opacify(0%), luma(100%, 0%))
+
+--- repr-color ---
+// Colors
+#set page(width: 400pt)
+#set text(0.8em)
+#blue \
+#color.linear-rgb(blue) \
+#oklab(blue) \
+#oklch(blue) \
+#cmyk(blue) \
+#color.hsl(blue) \
+#color.hsv(blue) \
+#luma(blue)
diff --git a/tests/suite/visualize/ellipse.typ b/tests/suite/visualize/ellipse.typ
new file mode 100644
index 00000000..970a795e
--- /dev/null
+++ b/tests/suite/visualize/ellipse.typ
@@ -0,0 +1,31 @@
+// Test the `ellipse` function.
+
+--- ellipse ---
+// Default ellipse.
+#ellipse()
+
+--- ellipse-auto-sizing ---
+#set rect(inset: 0pt)
+#set ellipse(inset: 0pt)
+
+Rect in ellipse in fixed rect.
+#rect(width: 3cm, height: 2cm, fill: rgb("2a631a"),
+ ellipse(fill: forest, width: 100%, height: 100%,
+ rect(fill: conifer, width: 100%, height: 100%,
+ align(center + horizon)[
+ Stuff inside an ellipse!
+ ]
+ )
+ )
+)
+
+Auto-sized ellipse.
+#ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[
+ #set text(8pt)
+ But, soft! what light through yonder window breaks?
+]
+
+
+An inline
+#box(ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)))
+ellipse.
diff --git a/tests/suite/visualize/gradient.typ b/tests/suite/visualize/gradient.typ
new file mode 100644
index 00000000..1ee5489a
--- /dev/null
+++ b/tests/suite/visualize/gradient.typ
@@ -0,0 +1,631 @@
+--- gradient-linear-angled ---
+// Test gradients with direction.
+#set page(width: 90pt)
+#grid(
+ gutter: 3pt,
+ columns: 4,
+ ..range(0, 360, step: 15).map(i => box(
+ height: 15pt,
+ width: 15pt,
+ fill: gradient.linear(angle: i * 1deg, (red, 0%), (blue, 100%)),
+ ))
+)
+
+
+--- gradient-linear-oklab ---
+// The tests below test whether hue rotation works correctly.
+// Here we test in Oklab space for reference.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: oklab)
+)
+
+--- gradient-linear-oklch ---
+// Test in OkLCH space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: oklch)
+)
+
+--- gradient-linear-hsv ---
+// Test in HSV space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: color.hsv)
+)
+
+--- gradient-linear-hsl ---
+// Test in HSL space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: color.hsl)
+)
+
+
+--- gradient-linear-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-linear-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.linear(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-linear-repeat-and-mirror-1 ---
+// Test repeated gradients.
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.inferno).repeat(2, mirror: true)
+)
+
+--- gradient-linear-repeat-and-mirror-2 ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).repeat(2, mirror: true),
+)
+
+--- gradient-linear-repeat-and-mirror-3 ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).repeat(5, mirror: true)
+)
+
+--- gradient-linear-sharp-and-repeat ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: false)
+)
+
+--- gradient-linear-sharp-repeat-and-mirror ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: true)
+)
+
+--- gradient-linear-sharp ---
+#square(
+ size: 100pt,
+ fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+#square(
+ size: 100pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+
+--- gradient-linear-sharp-and-smooth ---
+#square(
+ size: 100pt,
+ fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+#square(
+ size: 100pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+
+--- gradient-linear-stroke ---
+#align(center + top, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
+
+--- gradient-fill-and-stroke ---
+#align(
+ center + bottom,
+ square(
+ size: 50pt,
+ fill: gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%)),
+ stroke: 10pt + gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%))
+ )
+)
+
+--- gradient-linear-line ---
+// Test gradient on lines
+#set page(width: 100pt, height: 100pt)
+#line(length: 100%, stroke: 1pt + gradient.linear(red, blue))
+#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue))
+#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue, relative: "parent"))
+
+--- gradient-radial-hsl ---
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl),
+)
+
+--- gradient-radial-center ---
+#grid(
+ columns: 2,
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 0%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 100%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 0%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 100%)),
+ ),
+)
+
+--- gradient-radial-radius ---
+#square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 10%),
+)
+#square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 72%),
+)
+
+--- gradient-radial-focal-center-and-radius ---
+#circle(
+ radius: 25pt,
+ fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (35%, 35%), focal-radius: 5%),
+)
+#circle(
+ radius: 25pt,
+ fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
+)
+
+--- gradient-radial-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.radial(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-radial-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.radial(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-radial-text ---
+// Test that gradient fills on text.
+// The solid bar gradients are used to make sure that all transforms are
+// correct: if you can see the text through the bar, then the gradient is
+// misaligned to its reference container.
+#set page(width: 200pt, height: auto, margin: 10pt)
+#set par(justify: true)
+#set text(fill: gradient.radial(red, blue))
+#lorem(30)
+
+--- gradient-conic ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv),
+)
+
+--- gradient-conic-center-shifted-1 ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (10%, 10%)),
+)
+
+--- gradient-conic-center-shifted-2 ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (90%, 90%)),
+)
+
+--- gradient-conic-angled ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, angle: 90deg),
+)
+
+--- gradient-conic-oklab ---
+// Test in Oklab space for reference.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: oklab)
+)
+
+--- gradient-conic-oklch ---
+// Test in OkLCH space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: oklch)
+)
+
+--- gradient-conic-hsv ---
+// Test in HSV space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: color.hsv)
+)
+
+--- gradient-conic-hsl ---
+// Test in HSL space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: color.hsl)
+)
+
+--- gradient-conic-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.conic(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-conic-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.conic(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-conic-stroke ---
+#align(
+ center + bottom,
+ square(
+ size: 50pt,
+ fill: black,
+ stroke: 10pt + gradient.conic(red, blue)
+ )
+)
+
+--- gradient-conic-text ---
+#set page(width: 200pt, height: auto, margin: 10pt)
+#set par(justify: true)
+#set text(fill: gradient.conic(red, blue, angle: 45deg))
+#lorem(30)
+
+--- gradient-text-bad-relative ---
+// Make sure they don't work when `relative: "self"`.
+// Hint: 17-61 make sure to set `relative: auto` on your text fill
+// Error: 17-61 gradients and patterns on text must be relative to the parent
+#set text(fill: gradient.linear(red, blue, relative: "self"))
+
+--- gradient-text-global ---
+// Test that gradient fills on text work for globally defined gradients.
+#set page(width: 200pt, height: auto, margin: 10pt, background: {
+ rect(width: 100%, height: 30pt, fill: gradient.linear(red, blue))
+})
+#set par(justify: true)
+#set text(fill: gradient.linear(red, blue))
+#lorem(30)
+
+--- gradient-text-dir ---
+// Sanity check that the direction works on text.
+#set page(width: 200pt, height: auto, margin: 10pt, background: {
+ rect(height: 100%, width: 30pt, fill: gradient.linear(dir: btt, red, blue))
+})
+#set par(justify: true)
+#set text(fill: gradient.linear(dir: btt, red, blue))
+#lorem(30)
+
+--- gradient-text-in-container ---
+// Test that gradient fills on text work for locally defined gradients.
+#set page(width: auto, height: auto, margin: 10pt)
+#show box: set text(fill: gradient.linear(..color.map.rainbow))
+Hello, #box[World]!
+
+--- gradient-text-rotate ---
+// Test that gradients fills on text work with transforms.
+#set page(width: auto, height: auto, margin: 10pt)
+#show box: set text(fill: gradient.linear(..color.map.rainbow))
+#rotate(45deg, box[World])
+
+--- gradient-text-decoration ---
+#set text(fill: gradient.linear(red, blue))
+
+Hello #underline[World]! \
+Hello #overline[World]! \
+Hello #strike[World]! \
+
+--- gradient-transformed ---
+// Test whether gradients work well when they are contained within a transform.
+#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50pt, height: 50pt, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+)
+#place(top + right, scale(x: 200%, y: 130%, my-rect))
+#place(bottom + center, rotate(45deg, my-rect))
+#place(horizon + center, scale(x: 200%, y: 130%, rotate(45deg, my-rect)))
+
+--- gradient-presets ---
+// Test all gradient presets.
+#set page(width: 100pt, height: auto, margin: 0pt)
+#set text(fill: white, size: 18pt)
+#set text(top-edge: "bounds", bottom-edge: "bounds")
+
+#let presets = (
+ ("turbo", color.map.turbo),
+ ("cividis", color.map.cividis),
+ ("rainbow", color.map.rainbow),
+ ("spectral", color.map.spectral),
+ ("viridis", color.map.viridis),
+ ("inferno", color.map.inferno),
+ ("magma", color.map.magma),
+ ("plasma", color.map.plasma),
+ ("rocket", color.map.rocket),
+ ("mako", color.map.mako),
+ ("vlag", color.map.vlag),
+ ("icefire", color.map.icefire),
+ ("flare", color.map.flare),
+ ("crest", color.map.crest),
+)
+
+#stack(
+ spacing: 3pt,
+ ..presets.map(((name, preset)) => block(
+ width: 100%,
+ height: 20pt,
+ fill: gradient.linear(..preset),
+ align(center + horizon, smallcaps(name)),
+ ))
+)
+
+// Test that gradients are applied correctly on equations.
+
+--- gradient-math-cancel ---
+// Test on cancel
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ a dot cancel(5) = cancel(25) 5 x + cancel(5) 1 $
+
+--- gradient-math-frac ---
+// Test on frac
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ nabla dot bold(E) = frac(rho, epsilon_0) $
+
+--- gradient-math-root ---
+// Test on root
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ x_"1,2" = frac(-b +- sqrt(b^2 - 4 a c), 2 a) $
+
+--- gradient-math-mat ---
+// Test on matrix
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+--- gradient-math-underover ---
+// Test on underover
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ underline(X^2) $
+$ overline("hello, world!") $
+
+--- gradient-math-dir ---
+// Test a different direction
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow, dir: ttb))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+$ x_"1,2" = frac(-b +- sqrt(b^2 - 4 a c), 2 a) $
+
+--- gradient-math-misc ---
+// Test miscellaneous
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ hat(x) = bar x bar = vec(x, y, z) = tilde(x) = dot(x) $
+$ x prime = vec(1, 2, delim: "[") $
+$ sum_(i in NN) 1 + i $
+$ attach(
+ Pi, t: alpha, b: beta,
+ tl: 1, tr: 2+3, bl: 4+5, br: 6,
+) $
+
+--- gradient-math-radial ---
+// Test radial gradient
+#show math.equation: set text(fill: gradient.radial(..color.map.rainbow, center: (30%, 30%)))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+--- gradient-math-conic ---
+// Test conic gradient
+#show math.equation: set text(fill: gradient.conic(red, blue, angle: 45deg))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+
+--- gradient-kind ---
+// Test gradient functions.
+#test(gradient.linear(red, green, blue).kind(), gradient.linear)
+
+--- gradient-stops ---
+#test(gradient.linear(red, green, blue).stops(), ((red, 0%), (green, 50%), (blue, 100%)))
+
+--- gradient-sample ---
+#test(gradient.linear(red, green, blue, space: rgb).sample(0%), red)
+#test(gradient.linear(red, green, blue, space: rgb).sample(25%), rgb("#97873b"))
+#test(gradient.linear(red, green, blue, space: rgb).sample(50%), green)
+#test(gradient.linear(red, green, blue, space: rgb).sample(75%), rgb("#17a08c"))
+#test(gradient.linear(red, green, blue, space: rgb).sample(100%), blue)
+
+--- gradient-space ---
+#test(gradient.linear(red, green, space: rgb).space(), rgb)
+#test(gradient.linear(red, green, space: oklab).space(), oklab)
+#test(gradient.linear(red, green, space: oklch).space(), oklch)
+#test(gradient.linear(red, green, space: cmyk).space(), cmyk)
+#test(gradient.linear(red, green, space: luma).space(), luma)
+#test(gradient.linear(red, green, space: color.linear-rgb).space(), color.linear-rgb)
+#test(gradient.linear(red, green, space: color.hsl).space(), color.hsl)
+#test(gradient.linear(red, green, space: color.hsv).space(), color.hsv)
+
+--- gradient-relative ---
+#test(gradient.linear(red, green, relative: "self").relative(), "self")
+#test(gradient.linear(red, green, relative: "parent").relative(), "parent")
+#test(gradient.linear(red, green).relative(), auto)
+
+--- gradient-angle ---
+#test(gradient.linear(red, green).angle(), 0deg)
+#test(gradient.linear(red, green, dir: ltr).angle(), 0deg)
+#test(gradient.linear(red, green, dir: rtl).angle(), 180deg)
+#test(gradient.linear(red, green, dir: ttb).angle(), 90deg)
+#test(gradient.linear(red, green, dir: btt).angle(), 270deg)
+
+--- gradient-repeat ---
+#test(
+ gradient.linear(red, green, blue).repeat(2).stops(),
+ ((red, 0%), (green, 25%), (blue, 50%), (red, 50%), (green, 75%), (blue, 100%))
+)
+#test(
+ gradient.linear(red, green, blue).repeat(2, mirror: true).stops(),
+ ((red, 0%), (green, 25%), (blue, 50%), (green, 75%), (red, 100%))
+)
+
+--- gradient-repr ---
+// Gradients
+#set page(width: 400pt)
+#set text(0.8em)
+#gradient.linear(blue, red) \
+#gradient.linear(blue, red, dir: ttb) \
+#gradient.linear(blue, red, angle: 45deg, relative: "self") \
+#gradient.linear(blue, red, angle: 45deg, space: rgb)
+
+--- issue-2902-gradient-oklch-panic ---
+// Minimal reproduction of #2902
+#set page(width: 15cm, height: auto, margin: 1em)
+#set block(width: 100%, height: 1cm, above: 2pt)
+
+// Oklch
+#block(fill: gradient.linear(red, purple, space: oklch))
+#block(fill: gradient.linear(..color.map.rainbow, space: oklch))
+#block(fill: gradient.linear(..color.map.plasma, space: oklch))
+
+--- issue-2902-gradient-oklab-panic ---
+#set page(width: 15cm, height: auto, margin: 1em)
+#set block(width: 100%, height: 1cm, above: 2pt)
+
+// Oklab
+#block(fill: gradient.linear(red, purple, space: oklab))
+#block(fill: gradient.linear(..color.map.rainbow, space: oklab))
+#block(fill: gradient.linear(..color.map.plasma, space: oklab))
+
+--- issue-gradient-cmyk-encode ---
+// Test that CMYK works on gradients
+#set page(margin: 0pt, width: 100pt, height: auto)
+
+#let violet = cmyk(75%, 80%, 0%, 0%)
+#let blue = cmyk(75%, 30%, 0%, 0%)
+
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(violet, blue)
+)
+
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(rgb(violet), rgb(blue))
+)
+
+// In PDF format, this gradient can look different from the others.
+// This is because PDF readers do weird things with CMYK.
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(violet, blue, space: cmyk)
+)
diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ
new file mode 100644
index 00000000..ac2d5af9
--- /dev/null
+++ b/tests/suite/visualize/image.typ
@@ -0,0 +1,122 @@
+// Test the `image` function.
+
+--- image-rgba-png-and-jpeg ---
+// Test loading different image formats.
+
+// Load an RGBA PNG image.
+#image("/assets/images/rhino.png")
+
+// Load an RGB JPEG image.
+#set page(height: 60pt)
+#image("/assets/images/tiger.jpg")
+
+--- image-sizing ---
+// Test configuring the size and fitting behaviour of images.
+
+// Set width and height explicitly.
+#box(image("/assets/images/rhino.png", width: 30pt))
+#box(image("/assets/images/rhino.png", height: 30pt))
+
+// Set width and height explicitly and force stretching.
+#image("/assets/images/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
+
+// Make sure the bounding-box of the image is correct.
+#align(bottom + right, image("/assets/images/tiger.jpg", width: 40pt, alt: "A tiger"))
+
+--- image-fit ---
+// Test all three fit modes.
+#set page(height: 50pt, margin: 0pt)
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ rows: 100%,
+ gutter: 3pt,
+ image("/assets/images/tiger.jpg", width: 100%, height: 100%, fit: "contain"),
+ image("/assets/images/tiger.jpg", width: 100%, height: 100%, fit: "cover"),
+ image("/assets/images/monkey.svg", width: 100%, height: 100%, fit: "stretch"),
+)
+
+--- image-jump-to-next-page ---
+// Does not fit to remaining height of page.
+#set page(height: 60pt)
+Stuff
+#image("/assets/images/rhino.png")
+
+--- image-baseline-with-box ---
+// Test baseline.
+A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
+
+--- image-svg-complex ---
+// Test advanced SVG features.
+#image("/assets/images/pattern.svg")
+
+--- image-svg-text ---
+#set page(width: 250pt)
+
+#figure(
+ image("/assets/images/diagram.svg"),
+ caption: [A textful diagram],
+)
+
+--- image-svg-text-font ---
+#set page(width: 250pt)
+#show image: set text(font: ("Roboto", "Noto Serif CJK SC"))
+
+#figure(
+ image("/assets/images/chinese.svg"),
+ caption: [Bilingual text]
+)
+
+--- image-natural-dpi-sizing ---
+// Test that images aren't upscaled.
+// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
+// width, but rather max out at its natural size.
+#image("/assets/images/f2t.jpg")
+
+--- image-file-not-found ---
+// Error: 8-29 file not found (searched at tests/suite/visualize/path/does/not/exist)
+#image("path/does/not/exist")
+
+--- image-bad-format ---
+// Error: 2-22 unknown image format
+#image("./image.typ")
+
+--- image-bad-svg ---
+// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4)
+#image("/assets/images/bad.svg")
+
+--- image-decode-svg ---
+// Test parsing from svg data
+#image.decode(`<svg xmlns="http://www.w3.org/2000/svg" height="140" width="500"><ellipse cx="200" cy="80" rx="100" ry="50" style="fill:yellow;stroke:purple;stroke-width:2" /></svg>`.text, format: "svg")
+
+--- image-decode-bad-svg ---
+// Error: 2-168 failed to parse SVG (missing root node)
+#image.decode(`<svg height="140" width="500"><ellipse cx="200" cy="80" rx="100" ry="50" style="fill:yellow;stroke:purple;stroke-width:2" /></svg>`.text, format: "svg")
+
+--- image-decode-detect-format ---
+// Test format auto detect
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), width: 80%)
+
+--- image-decode-specify-format ---
+// Test format manual
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "jpg", width: 80%)
+
+--- image-decode-specify-wrong-format ---
+// Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.)
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%)
+
+--- issue-870-image-rotation ---
+// Ensure that EXIF rotation is applied.
+// https://github.com/image-rs/image/issues/1045
+// File is from https://magnushoff.com/articles/jpeg-orientation/
+#image("/assets/images/f2t.jpg", width: 10pt)
+
+--- issue-measure-image ---
+// Test that image measurement doesn't turn `inf / some-value` into 0pt.
+#context {
+ let size = measure(image("/assets/images/tiger.jpg"))
+ test(size, (width: 1024pt, height: 670pt))
+}
+
+--- issue-2051-new-cm-svg ---
+#set text(font: "New Computer Modern")
+#image("/assets/images/diagram.svg")
diff --git a/tests/suite/visualize/line.typ b/tests/suite/visualize/line.typ
new file mode 100644
index 00000000..7259f72b
--- /dev/null
+++ b/tests/suite/visualize/line.typ
@@ -0,0 +1,92 @@
+// Test lines.
+
+--- line-basic ---
+#set page(height: 60pt)
+#box({
+ set line(stroke: 0.75pt)
+ place(line(end: (0.4em, 0pt)))
+ place(line(start: (0pt, 0.4em), end: (0pt, 0pt)))
+ line(end: (0.6em, 0.6em))
+}) Hello #box(line(length: 1cm))!
+
+#line(end: (70%, 50%))
+
+--- line-positioning ---
+// Test the angle argument and positioning.
+
+#set page(fill: rgb("0B1026"))
+#set line(stroke: white)
+
+#let star(size, ..args) = box(width: size, height: size)[
+ #set text(spacing: 0%)
+ #set line(..args)
+ #set align(left)
+ #v(30%)
+ #place(line(length: +30%, start: (09.0%, 02%)))
+ #place(line(length: +30%, start: (38.7%, 02%), angle: -72deg))
+ #place(line(length: +30%, start: (57.5%, 02%), angle: 252deg))
+ #place(line(length: +30%, start: (57.3%, 02%)))
+ #place(line(length: -30%, start: (88.0%, 02%), angle: -36deg))
+ #place(line(length: +30%, start: (73.3%, 48%), angle: 252deg))
+ #place(line(length: -30%, start: (73.5%, 48%), angle: 36deg))
+ #place(line(length: +30%, start: (25.4%, 48%), angle: -36deg))
+ #place(line(length: +30%, start: (25.6%, 48%), angle: -72deg))
+ #place(line(length: +32%, start: (8.50%, 02%), angle: 34deg))
+]
+
+#align(center, grid(
+ columns: 3,
+ column-gutter: 10pt,
+ ..((star(20pt, stroke: 0.5pt),) * 9)
+))
+
+--- line-stroke ---
+// Some simple test lines
+#line(length: 60pt, stroke: red)
+#v(3pt)
+#line(length: 60pt, stroke: 2pt)
+#v(3pt)
+#line(length: 60pt, stroke: blue + 1.5pt)
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: "dashed"))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 4pt, cap: "round"))
+
+--- line-stroke-set ---
+// Set rules with stroke
+#set line(stroke: (paint: red, thickness: 1pt, cap: "butt", dash: "dash-dotted"))
+#line(length: 60pt)
+#v(3pt)
+#line(length: 60pt, stroke: blue)
+#v(3pt)
+#line(length: 60pt, stroke: (dash: none))
+
+--- line-stroke-dash ---
+// Dashing
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ("dot", 1pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ("dot", 1pt, 4pt, 2pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: (array: ("dot", 1pt, 4pt, 2pt), phase: 5pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ()))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: (1pt, 3pt, 9pt)))
+
+--- line-stroke-field-typo ---
+// Error: 29-56 unexpected key "thicknes", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit"
+#line(length: 60pt, stroke: (paint: red, thicknes: 1pt))
+
+--- line-stroke-bad-dash-kind ---
+// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, none, or auto
+#line(length: 60pt, stroke: (paint: red, dash: "dash"))
+
+--- line-bad-point-array ---
+// Test errors.
+
+// Error: 12-19 point array must contain exactly two entries
+#line(end: (50pt,))
+
+--- line-bad-point-component-type ---
+// Error: 14-26 expected relative length, found angle
+#line(start: (3deg, 10pt), length: 5cm)
diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ
new file mode 100644
index 00000000..10955f14
--- /dev/null
+++ b/tests/suite/visualize/path.typ
@@ -0,0 +1,52 @@
+// Test paths.
+
+--- path ---
+#set page(height: 200pt, width: 200pt)
+#table(
+ columns: (1fr, 1fr),
+ rows: (1fr, 1fr),
+ align: center + horizon,
+ path(
+ fill: red,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+ ),
+ path(
+ fill: purple,
+ stroke: 1pt,
+ (0pt, 0pt),
+ (30pt, 30pt),
+ (0pt, 30pt),
+ (30pt, 0pt),
+ ),
+ path(
+ fill: blue,
+ stroke: 1pt,
+ closed: true,
+ ((30%, 0%), (35%, 30%), (-20%, 0%)),
+ ((30%, 60%), (-20%, 0%), (0%, 0%)),
+ ((50%, 30%), (60%, -30%), (60%, 0%)),
+ ),
+ path(
+ stroke: 5pt,
+ closed: true,
+ (0pt, 30pt),
+ (30pt, 30pt),
+ (15pt, 0pt),
+ ),
+)
+
+--- path-bad-vertex ---
+// Error: 7-9 path vertex must have 1, 2, or 3 points
+#path(())
+
+--- path-bad-point-count ---
+// Error: 7-47 path vertex must have 1, 2, or 3 points
+#path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%)))
+
+--- path-bad-point-array ---
+// Error: 7-31 point array must contain exactly two entries
+#path(((0%, 0%), (0%, 0%, 0%)))
diff --git a/tests/suite/visualize/pattern.typ b/tests/suite/visualize/pattern.typ
new file mode 100644
index 00000000..08051ed2
--- /dev/null
+++ b/tests/suite/visualize/pattern.typ
@@ -0,0 +1,131 @@
+// Test patterns.
+
+--- pattern-line ---
+// Tests that simple patterns work.
+#set page(width: auto, height: auto, margin: 0pt)
+#let pat = pattern(size: (10pt, 10pt), line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%)))
+#rect(width: 50pt, height: 50pt, fill: pat)
+
+--- pattern-lines ---
+#set page(width: auto, height: auto, margin: 0pt)
+
+#let pat = pattern(size: (10pt, 10pt), {
+ place(line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%)))
+ place(line(stroke: 4pt, start: (100%,0%), end: (200%, 100%)))
+ place(line(stroke: 4pt, start: (0%,100%), end: (100%, 200%)))
+ place(line(stroke: 4pt, start: (-100%,0%), end: (0%, 100%)))
+ place(line(stroke: 4pt, start: (0%,-100%), end: (100%, 0%)))
+})
+#rect(width: 50pt, height: 50pt, fill: pat)
+
+--- pattern-relative-self ---
+// Test with relative set to `"self"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
+ #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
+]
+
+#set page(fill: pat(), width: 100pt, height: 100pt)
+
+#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-relative-parent ---
+// Test with relative set to `"parent"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
+ #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
+]
+
+#set page(fill: pat(), width: 100pt, height: 100pt)
+
+#rect(fill: pat(relative: "parent"), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-small ---
+// Tests small patterns for pixel accuracy.
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (1pt, 1pt), square(size: 1pt, fill: black))
+)
+#v(-1em)
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (2pt, 1pt), square(size: 1pt, fill: black))
+)
+
+--- pattern-zero-sized ---
+// Error: 15-52 pattern tile size must be non-zero
+// Hint: 15-52 try setting the size manually
+#line(stroke: pattern(path((0pt, 0pt), (1em, 0pt))))
+
+--- pattern-spacing-negative ---
+// Test with spacing set to `(-10pt, -10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (-10pt, -10pt)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-spacing-zero ---
+// Test with spacing set to `(0pt, 0pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (0pt, 0pt)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-spacing-positive ---
+// Test with spacing set to `(10pt, 10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (10pt, 10pt,)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-stroke ---
+// Test pattern on strokes
+#align(
+ center + top,
+ square(
+ size: 50pt,
+ stroke: 5pt + pattern(
+ size: (5pt, 5pt),
+ align(horizon + center, circle(fill: blue, radius: 2.5pt))
+ )
+ )
+)
+
+--- pattern-text ---
+// Test a pattern on some text
+// You shouldn't be able to see the text, if you can then
+// that means that the transform matrices are not being
+// applied to the text correctly.
+#let pat = pattern(
+ size: (30pt, 30pt),
+ relative: "parent",
+ square(size: 30pt, fill: gradient.conic(..color.map.rainbow))
+);
+
+#set page(
+ width: 140pt,
+ height: 140pt,
+ fill: pat
+)
+
+#rotate(45deg, scale(x: 50%, y: 70%, rect(
+ width: 100%,
+ height: 100%,
+ stroke: 1pt,
+)[
+ #lorem(10)
+
+ #set text(fill: pat)
+ #lorem(10)
+]))
diff --git a/tests/suite/visualize/polygon.typ b/tests/suite/visualize/polygon.typ
new file mode 100644
index 00000000..a3f4c8ef
--- /dev/null
+++ b/tests/suite/visualize/polygon.typ
@@ -0,0 +1,51 @@
+// Test polygons.
+
+--- polygon ---
+#set page(width: 50pt)
+#set polygon(stroke: 0.75pt, fill: blue)
+
+// These are not visible, but should also not give an error
+#polygon()
+#polygon((0em, 0pt))
+#polygon((0pt, 0pt), (10pt, 0pt))
+#polygon.regular(size: 0pt, vertices: 9)
+
+#polygon((5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+#polygon(
+ (0pt, 0pt), (5pt, 5pt), (10pt, 0pt),
+ (15pt, 5pt),
+ (5pt, 10pt)
+)
+#polygon(stroke: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+#polygon(stroke: 3pt, fill: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+
+// Relative size
+#polygon((0pt, 0pt), (100%, 5pt), (50%, 10pt))
+
+// Antiparallelogram
+#polygon((0pt, 5pt), (5pt, 0pt), (0pt, 10pt), (5pt, 15pt))
+
+// Self-intersections
+#polygon((0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt))
+
+// Regular polygon; should have equal side lengths
+#for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)}
+
+--- polygon-line-join ---
+// Line joins
+#stack(
+ dir: ltr,
+ spacing: 1em,
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "round"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "bevel"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "miter"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "miter", miter-limit: 20.0),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+)
+
+--- polygon-bad-point-array ---
+// Error: 10-17 point array must contain exactly two entries
+#polygon((50pt,))
diff --git a/tests/suite/visualize/rect.typ b/tests/suite/visualize/rect.typ
new file mode 100644
index 00000000..f84fafcb
--- /dev/null
+++ b/tests/suite/visualize/rect.typ
@@ -0,0 +1,107 @@
+// Test the `rect` function.
+
+--- rect ---
+// Default rectangle.
+#rect()
+
+--- rect-customization ---
+#set page(width: 150pt)
+
+// Fit to text.
+#rect(fill: conifer)[Textbox]
+
+// Empty with fixed width and height.
+#block(rect(
+ height: 15pt,
+ fill: rgb("46b3c2"),
+ stroke: 2pt + rgb("234994"),
+))
+
+// Fixed width, text height.
+#rect(width: 2cm, fill: rgb("9650d6"))[Fixed and padded]
+
+// Page width, fixed height.
+#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
+
+// These are inline with text.
+{#box(rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67")))
+ #box(rect(width: 0.5in, height: 7pt, fill: rgb("edd466")))
+ #box(rect(width: 0.5in, height: 7pt, fill: rgb("e3be62")))}
+
+// Rounded corners.
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ rect(width: 2cm, radius: 30%),
+ rect(width: 1cm, radius: (left: 10pt, right: 5pt)),
+ rect(width: 1.25cm, radius: (
+ top-left: 2pt,
+ top-right: 5pt,
+ bottom-right: 8pt,
+ bottom-left: 11pt
+ )),
+)
+
+// Different strokes.
+#set rect(stroke: (right: red))
+#rect(width: 100%, fill: lime, stroke: (x: 5pt, y: 1pt))
+
+--- rect-stroke ---
+// Rectangle strokes
+#rect(width: 20pt, height: 20pt, stroke: red)
+#v(3pt)
+#rect(width: 20pt, height: 20pt, stroke: (rest: red, top: (paint: blue, dash: "dashed")))
+#v(3pt)
+#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
+
+--- red-stroke-bad-type ---
+// Error: 15-21 expected length, color, gradient, pattern, dictionary, stroke, none, or auto, found array
+#rect(stroke: (1, 2))
+
+--- rect-fill-stroke ---
+#let variant = rect.with(width: 20pt, height: 10pt)
+#let items = for (i, item) in (
+ variant(stroke: none),
+ variant(),
+ variant(fill: none),
+ variant(stroke: 2pt),
+ variant(stroke: eastern),
+ variant(stroke: eastern + 2pt),
+ variant(fill: eastern),
+ variant(fill: eastern, stroke: none),
+ variant(fill: forest, stroke: none),
+ variant(fill: forest, stroke: conifer),
+ variant(fill: forest, stroke: black + 2pt),
+ variant(fill: forest, stroke: conifer + 2pt),
+).enumerate() {
+ (align(horizon)[#(i + 1).], item, [])
+}
+
+#grid(
+ columns: (auto, auto, 1fr, auto, auto, 0fr),
+ gutter: 5pt,
+ ..items,
+)
+
+--- rect-radius-bad-key ---
+// Error: 15-38 unexpected key "cake", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
+#rect(radius: (left: 10pt, cake: 5pt))
+
+--- issue-1825-rect-overflow ---
+#set page(width: 17.8cm)
+#set par(justify: true)
+#rect(lorem(70))
+
+--- issue-3264-rect-negative-dimensions ---
+// Negative dimensions
+#rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse left]
+
+#rect(width: 1cm, fill: gradient.linear(red, blue))[Left]
+
+#align(center, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse center])
+
+#align(center, rect(width: 1cm, fill: gradient.linear(red, blue))[Center])
+
+#align(right, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse right])
+
+#align(right, rect(width: 1cm, fill: gradient.linear(red, blue))[Right])
diff --git a/tests/suite/visualize/square.typ b/tests/suite/visualize/square.typ
new file mode 100644
index 00000000..caa1fc21
--- /dev/null
+++ b/tests/suite/visualize/square.typ
@@ -0,0 +1,146 @@
+// Test the `square` function.
+
+--- square ---
+// Default square.
+#box(square())
+#box(square[hey!])
+
+--- square-auto-sized ---
+// Test auto-sized square.
+#square(fill: eastern)[
+ #set text(fill: white, weight: "bold")
+ Typst
+]
+
+--- square-relatively-sized-child ---
+// Test relative-sized child.
+#square(fill: eastern)[
+ #rect(width: 10pt, height: 5pt, fill: conifer)
+ #rect(width: 40%, height: 5pt, stroke: conifer)
+]
+
+--- square-contents-overflow ---
+// Test text overflowing height.
+#set page(width: 75pt, height: 100pt)
+#square(fill: conifer)[
+ But, soft! what light through yonder window breaks?
+]
+
+--- square-height-limited ---
+// Test that square does not overflow page.
+#set page(width: 100pt, height: 75pt)
+#square(fill: conifer)[
+ But, soft! what light through yonder window breaks?
+]
+
+--- square-size-width-and-height ---
+// Size wins over width and height.
+// Error: 09-20 unexpected argument: width
+#square(width: 10cm, height: 20cm, size: 1cm, fill: rgb("eb5278"))
+
+--- square-relative-size ---
+// Test relative width and height and size that is smaller
+// than default size.
+#set page(width: 120pt, height: 70pt)
+#set align(bottom)
+#let centered = align.with(center + horizon)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ square(width: 50%, centered[A]),
+ square(height: 50%),
+ stack(
+ square(size: 10pt),
+ square(size: 20pt, centered[B])
+ ),
+)
+
+--- square-circle-alignment ---
+// Test alignment in automatically sized square and circle.
+#set text(8pt)
+#box(square(inset: 4pt)[
+ Hey there, #align(center + bottom, rotate(180deg, [you!]))
+])
+#box(circle(align(center + horizon, [Hey.])))
+
+--- square-circle-overspecified ---
+// Test that minimum wins if both width and height are given.
+#stack(
+ dir: ltr,
+ spacing: 2pt,
+ square(width: 20pt, height: 40pt),
+ circle(width: 20%, height: 100pt),
+)
+
+--- square-height-limited-stack ---
+// Test square that is limited by region size.
+#set page(width: 20pt, height: 10pt, margin: 0pt)
+#stack(dir: ltr, square(fill: forest), square(fill: conifer))
+
+--- square-overflow ---
+// Test that square doesn't overflow due to its aspect ratio.
+#set page(width: 40pt, height: 25pt, margin: 5pt)
+#square(width: 100%)
+#square(width: 100%)[Hello there]
+
+--- square-size-relative-invalid ---
+// Size cannot be relative because we wouldn't know
+// relative to which axis.
+// Error: 15-18 expected length or auto, found ratio
+#square(size: 50%)
+
+--- square-rect-rounded ---
+#set square(size: 20pt, stroke: 4pt)
+
+// no radius for non-rounded corners
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0pt),
+ h(10pt),
+ square(radius: -10pt),
+)
+
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0%),
+ h(10pt),
+ square(radius: -10%),
+)
+
+// small values for small radius
+#stack(
+ dir: ltr,
+ square(radius: 1pt),
+ h(10pt),
+ square(radius: 5%),
+ h(10pt),
+ square(radius: 2pt),
+)
+
+// large values for large radius or circle
+#stack(
+ dir: ltr,
+ square(radius: 8pt),
+ h(10pt),
+ square(radius: 10pt),
+ h(10pt),
+ square(radius: 12pt),
+)
+
+#stack(
+ dir: ltr,
+ square(radius: 45%),
+ h(10pt),
+ square(radius: 50%),
+ h(10pt),
+ square(radius: 55%),
+)
+
+--- square-base ---
+// Test that square sets correct base for its content.
+#set page(height: 80pt)
+#square(width: 40%, rect(width: 60%, height: 80%))
diff --git a/tests/suite/visualize/stroke.typ b/tests/suite/visualize/stroke.typ
new file mode 100644
index 00000000..b03c96c5
--- /dev/null
+++ b/tests/suite/visualize/stroke.typ
@@ -0,0 +1,171 @@
+// Test lines.
+
+--- stroke-constructor ---
+// Converting to stroke
+#assert.eq(stroke(red).paint, red)
+#assert.eq(stroke(red).thickness, auto)
+#assert.eq(stroke(2pt).paint, auto)
+#assert.eq(stroke((cap: "round", paint: blue)).cap, "round")
+#assert.eq(stroke((cap: auto, paint: blue)).cap, auto)
+#assert.eq(stroke((cap: auto, paint: blue)).thickness, auto)
+
+// Constructing with named arguments
+#assert.eq(stroke(paint: blue, thickness: 8pt), 8pt + blue)
+#assert.eq(stroke(thickness: 2pt), stroke(2pt))
+#assert.eq(stroke(cap: "round").thickness, auto)
+#assert.eq(stroke(cap: "round", thickness: auto).thickness, auto)
+
+--- stroke-constructor-unknown-key ---
+// Error: 9-21 unexpected key "foo", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit"
+#stroke((foo: "bar"))
+
+--- stroke-fields-simple ---
+// Test stroke fields for simple strokes.
+#test((1em + blue).paint, blue)
+#test((1em + blue).thickness, 1em)
+#test((1em + blue).cap, auto)
+#test((1em + blue).join, auto)
+#test((1em + blue).dash, auto)
+#test((1em + blue).miter-limit, auto)
+
+--- stroke-fields-complex ---
+// Test complex stroke fields.
+#let r1 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: none))
+#let r2 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: (3pt, "dot", 4em)))
+#let r3 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", dash: (array: (3pt, "dot", 4em), phase: 5em)))
+#let s1 = r1.stroke
+#let s2 = r2.stroke
+#let s3 = r3.stroke
+#test(s1.paint, cmyk(1%, 2%, 3%, 4%))
+#test(s1.thickness, 4em + 2pt)
+#test(s1.cap, "round")
+#test(s1.join, "bevel")
+#test(s1.miter-limit, 5.0)
+#test(s3.miter-limit, auto)
+#test(s1.dash, none)
+#test(s2.dash, (array: (3pt, "dot", 4em), phase: 0pt))
+#test(s3.dash, (array: (3pt, "dot", 4em), phase: 5em))
+
+--- stroke-zero-thickness ---
+// 0pt strokes must function exactly like 'none' strokes and not draw anything
+#rect(width: 10pt, height: 10pt, stroke: none)
+#rect(width: 10pt, height: 10pt, stroke: 0pt)
+#rect(width: 10pt, height: 10pt, stroke: none, fill: blue)
+#rect(width: 10pt, height: 10pt, stroke: 0pt + red, fill: blue)
+
+#line(length: 30pt, stroke: 0pt)
+#line(length: 30pt, stroke: (paint: red, thickness: 0pt, dash: ("dot", 1pt)))
+
+#table(columns: 2, stroke: none)[A][B]
+#table(columns: 2, stroke: 0pt)[A][B]
+
+#path(
+ fill: red,
+ stroke: none,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+)
+
+#path(
+ fill: red,
+ stroke: 0pt,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+)
+
+--- stroke-text ---
+#set text(size: 20pt)
+#set page(width: auto)
+#let v = [测试字体Test]
+
+#text(stroke: 0.3pt + red, v)
+
+#text(stroke: 0.7pt + red, v)
+
+#text(stroke: 7pt + red, v)
+
+#text(stroke: (paint: blue, thickness: 1pt, dash: "dashed"), v)
+
+#text(stroke: 1pt + gradient.linear(..color.map.rainbow), v)
+
+--- stroke-folding ---
+// Test stroke folding.
+#let sq(..args) = box(square(size: 10pt, ..args))
+
+#set square(stroke: none)
+#sq()
+#set square(stroke: auto)
+#sq()
+#sq(fill: teal)
+#sq(stroke: 2pt)
+#sq(stroke: blue)
+#sq(fill: teal, stroke: blue)
+#sq(fill: teal, stroke: 2pt + blue)
+
+--- stroke-composition ---
+// Test stroke composition.
+#set square(stroke: 4pt)
+#set text(font: "Roboto")
+#stack(
+ dir: ltr,
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow + 8pt, right: green, bottom: blue + 2pt),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 100%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+)
+
+// Join between different solid strokes
+#set square(size: 20pt, stroke: 2pt)
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: blue, bottom: black + 2pt))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
+)
+
+// Join between solid and dotted strokes
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: (paint: blue, dash: "dotted"), bottom: (paint: black, dash: "dotted")))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
+)
+
+--- issue-3700-deformed-stroke ---
+// Test shape fill & stroke for specific values that used to make the stroke
+// deformed.
+#rect(
+ radius: 1mm,
+ width: 100%,
+ height: 10pt,
+ stroke: (left: rgb("46b3c2") + 16.0mm),
+)