Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MIN_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.9.0
25 changes: 25 additions & 0 deletions docs/transitions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,28 @@ For every new effect, add parameter/default/invalid-value coverage, sampled
geometry and image checks, repeated-playback memory checks, and a review bundle.
Record renderer, resolution, duration and frame-time distribution for performance
reviews; do not infer a performance pass from still-image tests.

## Fade: experimental, awaiting visual approval

Fade exchanges light between two stationary slides. An sRGB decode, linear-light
blend, and sRGB encode keep white artwork steady and avoid a dark midpoint.
The blend weight is smoothstep (`3t² − 2t³`), with gentle starts and finishes.
Each slide retains its own fitted aspect ratio; the surrounding background is
part of each composition. Both full-resolution textures are ready before motion,
and endpoints use the exact static page path.

PDF `/S /Fade` uses `/Trans /D` for animation duration. `--program 4:fade` uses
the normal 750 ms programmed animation. Direction and axis have no effect.
Try [fade.pdf](../../examples/transitions/fade.pdf).

Candidate headless software-renderer captures (light, dark, and colored slides):

![Fade review checkpoints](fade-review/contact-sheet.png)

Animated previews: [light](fade-review/white.gif), [dark](fade-review/dark.gif),
[colored](fade-review/color.gif). These synthetic artwork pairs check shared
labels and brightness; photographic material still needs visual review.
Regenerate these previews with `python3 tools/fade_review.py` after building.
Tests compare rendered pixels against the sRGB transfer function at three
intermediate times, including mixed aspect ratios, and verify unchanged identical
slides, deterministic output, PDF metadata, CLI dispatch, and exact endpoints.
Binary file added docs/transitions/fade-review/color.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/color.pdf
Binary file not shown.
Binary file added docs/transitions/fade-review/color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/contact-sheet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/dark.pdf
Binary file not shown.
Binary file added docs/transitions/fade-review/dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/white.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/fade-review/white.pdf
Binary file not shown.
Binary file added docs/transitions/fade-review/white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions examples/transitions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,8 @@ normal Space/Page Down playback or `--program 4:wipe`.

`glitter-right.pdf`, `glitter-down.pdf` and `glitter-down-right.pdf` demonstrate
the experimental directional flare and sparks transition. Press Space to advance.

## Fade

[fade.pdf](fade.pdf) demonstrates a stationary, linear-light crossfade with gently
eased timing. Press Space to advance; `/Trans /D` sets its 1.25-second duration.
Binary file added examples/transitions/fade.pdf
Binary file not shown.
2 changes: 2 additions & 0 deletions src/playback.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ inline Transition transition_from_name(const std::string& effect) {
if (effect == "push") return Transition::push;
if (effect == "dissolve") return Transition::dissolve;
if (effect == "wipe") return Transition::wipe;
if (effect == "fade") return Transition::fade;
if (effect == "glitter") return Transition::glitter;
return Transition::switch_page;
}
Expand All @@ -34,6 +35,7 @@ inline Transition transition_from_page(const PageInfo& page) {
case POPPLER_PAGE_TRANSITION_PUSH: return Transition::push;
case POPPLER_PAGE_TRANSITION_DISSOLVE: return Transition::dissolve;
case POPPLER_PAGE_TRANSITION_WIPE: return Transition::wipe;
case POPPLER_PAGE_TRANSITION_FADE: return Transition::fade;
case POPPLER_PAGE_TRANSITION_GLITTER: return Transition::glitter;
default: return Transition::switch_page;
}
Expand Down
75 changes: 75 additions & 0 deletions src/renderer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,32 @@ uniform vec4 solid_color;
uniform bool textured;
uniform float shade;
out vec4 output_color;
uniform bool fading;
uniform sampler2D incoming_texture;
uniform vec4 outgoing_fit;
uniform vec4 incoming_fit;
uniform vec3 fade_background;
uniform float fade_weight;
vec3 linear_light(vec3 c) {
return mix(c / 12.92, pow((c + .055) / 1.055, vec3(2.4)), step(vec3(.04045), c));
}
vec3 encoded_light(vec3 c) {
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - .055, step(vec3(.0031308), c));
}
vec3 fitted_page(sampler2D image, vec4 fit) {
vec2 local = (uv - fit.xy) / fit.zw;
if (any(lessThan(local, vec2(0))) || any(greaterThan(local, vec2(1))))
return fade_background;
return texture(image, local).rgb;
}
void main() {
if (fading) {
vec3 a = linear_light(fitted_page(page_texture, outgoing_fit));
vec3 b = linear_light(fitted_page(incoming_texture, incoming_fit));
output_color = vec4(encoded_light(mix(a, b, fade_weight)), 1);
return;
}

output_color = textured ? texture(page_texture, uv) : solid_color;
output_color = mix(output_color, vec4(.80, .90, .96, 1.0), rim);
output_color.rgb *= shade * light;
Expand Down Expand Up @@ -175,7 +200,32 @@ uniform vec4 solid_color;
uniform bool textured;
uniform float shade;
out vec4 output_color;
uniform bool fading;
uniform sampler2D incoming_texture;
uniform vec4 outgoing_fit;
uniform vec4 incoming_fit;
uniform vec3 fade_background;
uniform float fade_weight;
vec3 linear_light(vec3 c) {
return mix(c / 12.92, pow((c + .055) / 1.055, vec3(2.4)), step(vec3(.04045), c));
}
vec3 encoded_light(vec3 c) {
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - .055, step(vec3(.0031308), c));
}
vec3 fitted_page(sampler2D image, vec4 fit) {
vec2 local = (uv - fit.xy) / fit.zw;
if (any(lessThan(local, vec2(0))) || any(greaterThan(local, vec2(1))))
return fade_background;
return texture(image, local).rgb;
}
void main() {
if (fading) {
vec3 a = linear_light(fitted_page(page_texture, outgoing_fit));
vec3 b = linear_light(fitted_page(incoming_texture, incoming_fit));
output_color = vec4(encoded_light(mix(a, b, fade_weight)), 1);
return;
}

output_color = textured ? texture(page_texture, uv) : solid_color;
output_color = mix(output_color, vec4(.80, .90, .96, 1.0), rim);
output_color.rgb *= shade * light;
Expand Down Expand Up @@ -543,6 +593,7 @@ void Renderer::render(int width, int height, const FrameState& frame) {

glUniform4f(uv_rect_uniform_, 0, 0, 1, 1);
glUniform1f(shade_uniform_, 1);
glUniform1i(glGetUniformLocation(program_, "fading"), GL_FALSE);
const bool panels = page_effect(frame.transition);
const bool landed = panels && frame.progress >= 1;
const auto& page = document_.page_info(landed ? frame.next_page : frame.current_page);
Expand Down Expand Up @@ -641,6 +692,30 @@ void Renderer::render(int width, int height, const FrameState& frame) {
glVertexAttrib1f(3, 0);
return;
}
if (frame.transition == Transition::fade) {
// Blend complete slide compositions, including independently fitted margins.
glViewport(0, 0, width, height);
const auto fit_uniform = [&](const char* name, int index) {
const auto& info = document_.page_info(index);
const auto fit = content_viewport(width, height, info.width / info.height);
glUniform4f(glGetUniformLocation(program_, name),
float(fit.x) / width, float(height - fit.y - fit.height) / height,
float(fit.width) / width, float(fit.height) / height);
};
fit_uniform("outgoing_fit", frame.current_page);
fit_uniform("incoming_fit", frame.next_page);
glUniform3f(glGetUniformLocation(program_, "fade_background"),
background_.red, background_.green, background_.blue);
const float t = static_cast<float>(frame.progress);
glUniform1f(glGetUniformLocation(program_, "fade_weight"), t * t * (3 - 2 * t));
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture_for_page(frame.next_page, page_size(frame.next_page)));
glUniform1i(glGetUniformLocation(program_, "incoming_texture"), 1);
glUniform1i(glGetUniformLocation(program_, "fading"), GL_TRUE);
draw_page(frame.current_page, page_size(frame.current_page), Matrix::identity().value.data());
glUniform1i(glGetUniformLocation(program_, "fading"), GL_FALSE);
return;
}
if (frame.transition == Transition::push) {
const PushPose pose = push_pose(frame.progress, frame.direction_degrees);
const Matrix view = perspective(2 * std::atan(1 / flip_camera), 1, .5F, 10) *
Expand Down
4 changes: 2 additions & 2 deletions src/renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ namespace pdfcube {

enum class Transition {
none, cube_forward, cube_backward, switch_page, blinds, split, cover, uncover, fly, push,
dissolve, glitter, wipe
dissolve, glitter, wipe, fade
};

inline bool page_effect(Transition transition) {
return transition == Transition::blinds || transition == Transition::split ||
transition == Transition::cover || transition == Transition::uncover ||
transition == Transition::fly || transition == Transition::push ||
transition == Transition::dissolve || transition == Transition::glitter ||
transition == Transition::wipe;
transition == Transition::wipe || transition == Transition::fade;
}

struct FrameState {
Expand Down
2 changes: 1 addition & 1 deletion tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ if host_machine.system() == 'linux'
render_environment.set('FONTCONFIG_FILE', meson.current_source_dir() / 'fonts.conf')
render_environment.set('XDG_CACHE_HOME', meson.current_build_dir() / 'native-test-cache')
# Every case targets 500 ms, including cold setup. Serialize to avoid GPU contention.
foreach effect : ['Blinds', 'Split', 'Cover', 'Uncover', 'Fly', 'Push', 'Dissolve', 'Glitter', 'Wipe']
foreach effect : ['Blinds', 'Split', 'Cover', 'Uncover', 'Fly', 'Push', 'Dissolve', 'Glitter', 'Wipe', 'Fade']
axes = ['H']
angles = ['none']
motions = ['I']
Expand Down
1 change: 1 addition & 0 deletions tests/test_playback.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ int main() {
{POPPLER_PAGE_TRANSITION_COVER,"cover"}, {POPPLER_PAGE_TRANSITION_UNCOVER,"uncover"},
{POPPLER_PAGE_TRANSITION_FLY,"fly"}, {POPPLER_PAGE_TRANSITION_PUSH,"push"},
{POPPLER_PAGE_TRANSITION_DISSOLVE,"dissolve"},
{POPPLER_PAGE_TRANSITION_FADE,"fade"},
{POPPLER_PAGE_TRANSITION_GLITTER,"glitter"},
{POPPLER_PAGE_TRANSITION_WIPE,"wipe"}};
for (const auto& [kind,name] : kinds) {
Expand Down
6 changes: 4 additions & 2 deletions tests/test_transition_metadata.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "document.hpp"
#include <cmath>
#include <cstdlib>
#include <string>

int main(int argc, char** argv) {
if (argc != 3 && argc != 5 && argc != 6 && argc != 8) return EXIT_FAILURE;
Expand All @@ -9,12 +10,13 @@ int main(int argc, char** argv) {
const bool split = argc >= 5 && argv[3][0] == 'S';
const bool cover = argc >= 5 && argv[3][0] == 'C';
const bool uncover = argc >= 5 && argv[3][0] == 'U';
const bool fly = argc >= 5 && argv[3][0] == 'F';
const bool fly = argc >= 5 && std::string(argv[3]) == "Fly";
const bool push = argc >= 5 && argv[3][0] == 'P';
const bool dissolve = argc >= 5 && argv[3][0] == 'D';
const bool wipe = argc >= 5 && argv[3][0] == 'W';
const bool glitter = argc >= 5 && argv[3][0] == 'G';
const auto type = split ? POPPLER_PAGE_TRANSITION_SPLIT :
const auto type = argc >= 5 && std::string(argv[3]) == "Fade" ? POPPLER_PAGE_TRANSITION_FADE :
split ? POPPLER_PAGE_TRANSITION_SPLIT :
cover ? POPPLER_PAGE_TRANSITION_COVER :
uncover ? POPPLER_PAGE_TRANSITION_UNCOVER :
fly ? POPPLER_PAGE_TRANSITION_FLY :
Expand Down
25 changes: 24 additions & 1 deletion tests/test_transitions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pathlib import Path
from io import BytesIO
from PIL import Image
import shlex
import subprocess
import sys
Expand Down Expand Up @@ -75,6 +77,27 @@ def capture(pdf, progress=None, *extra):
if dissolve:
assert capture(deck, .5, "--experimental") != middle, \
"experimental glass thickness was not rendered"
if effect == "Fade":
# Compare actual static compositions, including margins, to the
# sRGB transfer function rather than duplicating shader samples.
a, b = (Image.open(BytesIO(data)).convert("RGB") for data in (start, end))
def linear(c):
c /= 255
return c / 12.92 if c <= .04045 else ((c + .055) / 1.055) ** 2.4
def encoded(c):
return 255 * (12.92 * c if c <= .0031308 else 1.055 * c ** (1/2.4) - .055)
for t in (.25, .5, .75):
image = Image.open(BytesIO(capture(deck, t))).convert("RGB")
weight = t*t*(3-2*t)
for y in range(7, image.height, 19):
for x in range(7, image.width, 19):
expected = [encoded((1-weight)*linear(c)+weight*linear(d))
for c, d in zip(a.getpixel((x,y)), b.getpixel((x,y)))]
assert all(abs(c-d) <= 2 for c,d in zip(image.getpixel((x,y)),expected)), \
"fade changed brightness, fit, or easing"
identical = root / "identical.pdf"
make_pdf(identical, orientation, sizes, (0, 0), effect="Fade")
assert capture(identical, .5, "--program", "4:fade") == start, "shared artwork changed during fade"
if flip and angle == 0:
for value in (None, 45, -90):
fallback = root / "fallback.pdf"
Expand Down Expand Up @@ -127,7 +150,7 @@ def capture(pdf, progress=None, *extra):
assert capture(first) == start, "wiper leaked GL state into a static frame"
quarter, three_quarters = capture(deck, .25), capture(deck, .75)
assert len({start, quarter, middle, three_quarters, end}) == 5, "cascade stalled"
if not dissolve:
if not dissolve and effect != "Fade":
changed = root / "changed-direction.pdf"
if glitter:
make_pdf(changed, orientation, sizes, effect=effect,
Expand Down
1 change: 1 addition & 0 deletions tests/transition_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
EXAMPLES += [(f"wipe-{name}", "Wipe", "H", "I", angle)
for angle,name in ((0,"right"),(90,"up"),(180,"left"),(270,"down"))]

EXAMPLES += [("fade", "Fade", "H", "I", None)]

def make_pdf(path, orientation="H", sizes=((640, 360), (640, 360)), pages=(0, 1),
effect="Blinds", motion="I", angle=None, start_scale=.35, rectangular=True):
Expand Down
47 changes: 47 additions & 0 deletions tools/fade_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Generate synthetic Fade review strips and animations from the headless driver.

Run from the repository root after meson compile -C build. Requires Pillow.
All frames use explicit progress in a single headless context.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tests'))
from transition_fixture import make_pdf
from render_client import Frames
from PIL import Image, ImageDraw
root=Path.cwd()/'docs/transitions/fade-review'
root.mkdir(exist_ok=True)
# Each fixture is generated with valid xref offsets after changing its palette.
import transition_fixture as fixture
source=Path('tests/transition_fixture.py').read_text()
frames=Frames('build/tests/render-driver',root)
sheets=[]
for name,old,new in [('white','0.98 0.98 0.98','0.94 0.96 1'),('dark','0.015 0.025 0.06','0.1 0.025 0.015'),('color','0.12 0.32 0.65','0.65 0.23 0.12')]:
scope={'__name__':'preview'}
themed = source.replace('0.12 0.32 0.65',old).replace('0.65 0.23 0.12',new)
if name == 'white':
themed = themed.replace('1 1 1 RG', '0.15 0.15 0.15 RG').replace('1 1 1 rg', '0.15 0.15 0.15 rg')
exec(themed,scope)
pdf=root/f'{name}.pdf'
scope['make_pdf'](pdf,effect='Fade')
sheet=Image.new('RGB',(1600,210),'#202020')
animation=[]
for i in range(31):
output=root/f'{name}-frame.png'
frames.render('--bgcolor','0.04:0.04:0.04','--snapshot',output,'--snapshot-progress',str(i/30),pdf)
image=Image.open(output).convert('RGB'); image.thumbnail((640,360)); animation.append(image.copy())
for column,t in enumerate((0,.25,.5,.75,1)):
output=root/f'{name}-{column}.png'
frames.render('--bgcolor','0.04:0.04:0.04','--snapshot',output,'--snapshot-progress',str(t),pdf)
image=Image.open(output).convert('RGB');image.thumbnail((320,180))
sheet.paste(image,(column*320,30))
ImageDraw.Draw(sheet).text((column*320+8,8),f'{name} {t:.0%}',fill='white')
output.unlink()
sheet.save(root/f'{name}.png'); sheets.append(sheet)
animation[0].save(root/f'{name}.gif',save_all=True,append_images=animation[1:],duration=40,loop=0)
(root/f'{name}-frame.png').unlink()
frames.close()
combined=Image.new('RGB',(1600,630))
for i,sheet in enumerate(sheets):combined.paste(sheet,(0,i*210))
combined.save(root/'contact-sheet.png')
Loading