diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 1 | ||||
| -rw-r--r-- | src/config.zig | 49 | ||||
| -rw-r--r-- | src/macos.zig | 29 | ||||
| -rw-r--r-- | src/macos/Sources/AppDelegate.swift | 24 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 16 | ||||
| -rw-r--r-- | src/macos/pardes.h | 4 | ||||
| -rw-r--r-- | src/panes.zig | 7 | ||||
| -rw-r--r-- | src/pardes.zig | 2 | ||||
| -rw-r--r-- | src/pdf.zig | 48 | ||||
| -rw-r--r-- | src/pdf_bridge.c | 21 | ||||
| -rw-r--r-- | src/pdf_bridge.h | 6 |
11 files changed, 186 insertions, 21 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 1cd10a99..39d70e5d 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -18,6 +18,7 @@ pub const capabilities: config.Runtime.Capabilities = .{ // whatever terminal is on the other end of the serial line. .tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4, .window_opacity = pardes.platform == .gui or pardes.platform == .macos, + .window_blur = pardes.platform == .macos, }; pub const Ctx = struct { diff --git a/src/config.zig b/src/config.zig index f91d95c6..440549b3 100644 --- a/src/config.zig +++ b/src/config.zig @@ -102,6 +102,7 @@ pub const leader_path = paths: { } if (pardes.platform == .gui) table.set(.Pet, null); if (builtins.capabilities.window_opacity) table.set(.WindowOpacity, null); + if (builtins.capabilities.window_blur) table.set(.WindowBlur, null); if (builtins.capabilities.panel_transitions) { table.set(.PanelSlide, "as"); table.set(.PanelZoom, "az"); @@ -540,6 +541,7 @@ pub const Runtime = struct { column_tags: bool = true, syntax_bold: bool = false, window_opacity: u8 = 100, + window_blur: u8 = 0, window_opacity_pending: bool = true, pet: enum { off, cat, frog } = .off, @@ -630,6 +632,7 @@ pub const Runtime = struct { scene_shaders: bool, tagline_font_size: bool, window_opacity: bool = false, + window_blur: bool = false, }; pub const Capability = std.meta.FieldEnum(Capabilities); @@ -644,6 +647,7 @@ pub const Runtime = struct { font, tagline_size, window_opacity, + window_blur, transition: layout.Transition, scene: Scene, }; @@ -655,7 +659,7 @@ pub const Runtime = struct { pub fn takesArg(setting: Setting) bool { return switch (setting.action) { - .shell, .theme, .font, .tagline_size, .window_opacity => true, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur => true, else => false, }; } @@ -683,6 +687,7 @@ pub const Runtime = struct { .{ .word = "Font", .action = .font, .availability = .font_picker }, .{ .word = "TaglineSize", .action = .tagline_size, .availability = .font_picker }, .{ .word = "WindowOpacity", .action = .window_opacity, .availability = .window_opacity }, + .{ .word = "WindowBlur", .action = .window_blur, .availability = .window_blur }, .{ .word = "PanelSlide", .action = .{ .transition = .slide }, .availability = .panel_transitions }, .{ .word = "PanelZoom", .action = .{ .transition = .zoom }, .availability = .panel_transitions }, .{ .word = "PanelDissolve", .action = .{ .transition = .dissolve }, .availability = .panel_transitions }, @@ -726,6 +731,8 @@ pub const Runtime = struct { @compileError("native font settings must use the font-picker capability"), .window_opacity => if (setting.availability != .window_opacity) @compileError("window opacity must use the window-opacity capability"), + .window_blur => if (setting.availability != .window_blur) + @compileError("window blur must use the window-blur capability"), .transition => if (setting.availability != .panel_transitions) @compileError("panel effects must use the panel-transition capability"), .scene => if (setting.availability != .scene_shaders) @@ -737,8 +744,8 @@ pub const Runtime = struct { for (std.enums.values(Toggle)) |field| if (actionCount(.{ .toggle = field }) != 1) @compileError("runtime toggle must occur exactly once: " ++ @tagName(field)); if (actionCount(.shell) != 1 or actionCount(.theme) != 1 or actionCount(.font) != 1 or - actionCount(.tagline_size) != 1 or actionCount(.window_opacity) != 1) - @compileError("Shell, Theme, Font, TaglineSize, and WindowOpacity actions must each occur exactly once"); + actionCount(.tagline_size) != 1 or actionCount(.window_opacity) != 1 or actionCount(.window_blur) != 1) + @compileError("Shell, Theme, Font, TaglineSize, WindowOpacity, and WindowBlur actions must each occur exactly once"); for (std.enums.values(layout.Transition)) |effect| { const expected: comptime_int = @intFromBool(effect != .off); if (actionCount(.{ .transition = effect }) != expected) @@ -783,6 +790,13 @@ pub const Runtime = struct { state.window_opacity = percent; state.window_opacity_pending = true; }, + .window_blur => { + const text = std.mem.trim(u8, argument orelse return false, " \t\r\n"); + for (text) |byte| if (!std.ascii.isDigit(byte)) return false; + const percent = std.fmt.parseInt(u8, text, 10) catch return false; + if (percent > 100) return false; + state.window_blur = percent; + }, .transition => |effect| state.toggleTransition(effect), .scene => |effect| switch (effect) { inline else => |field| { @@ -849,6 +863,12 @@ pub const Runtime = struct { else try out.print("{s}: unsupported\n", .{setting.word}); }, + .window_blur => { + if (setting.enabled(context.capabilities)) + try out.print("{s}: {d}%\n", .{ setting.word, state.window_blur }) + else + try out.print("{s}: unsupported\n", .{setting.word}); + }, .shell => { const chosen = state.shell.requested.get(); try out.print( @@ -947,7 +967,7 @@ pub const Runtime = struct { for (settings[i + 1 ..]) |later| try std.testing.expect(!std.mem.eql(u8, setting.word, later.word)); try std.testing.expectEqual(switch (setting.action) { - .shell, .theme, .font, .tagline_size, .window_opacity => true, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur => true, else => false, }, setting.takesArg()); } @@ -1011,6 +1031,22 @@ pub const Runtime = struct { try std.testing.expect(state.window_opacity_pending); } + test "window blur defaults off and validates without changing opacity" { + const setting = find("WindowBlur").?; + var state: Runtime = .{}; + try std.testing.expectEqual(@as(u8, 0), state.window_blur); + try std.testing.expectEqual(Capability.window_blur, setting.availability.?); + for ([_][]const u8{ "0", " 40\n", "100" }, [_]u8{ 0, 40, 100 }) |argument, expected| { + try std.testing.expect(apply(&state, setting, argument)); + try std.testing.expectEqual(expected, state.window_blur); + try std.testing.expectEqual(@as(u8, 100), state.window_opacity); + } + for ([_]?[]const u8{ null, "", " \t\n", "101", "-1", "+40", "40%", "0.5", "40 20", "4_0", "0x28", "999999999999999" }) |argument| { + try std.testing.expect(!apply(&state, setting, argument)); + try std.testing.expectEqual(@as(u8, 100), state.window_blur); + } + } + test "font request tuple rejects atomically" { var state: Runtime = .{}; try std.testing.expect(requestFont(&state, "/fonts/old.ttf", "Old", 1200)); @@ -1052,6 +1088,7 @@ pub const Runtime = struct { .scene_shaders = true, .tagline_font_size = true, .window_opacity = true, + .window_blur = true, }, .state = &state, }; @@ -1065,6 +1102,7 @@ pub const Runtime = struct { .shell => "fish", .tagline_size => "73", .window_opacity => "85", + .window_blur => "40", else => null, }; try std.testing.expect(apply(&state, setting, argument)); @@ -1086,6 +1124,7 @@ pub const Runtime = struct { .shell => "Shell requested (new panes): fish\n", .tagline_size => "TaglineSize: 73%\n", .window_opacity => "WindowOpacity: 85% (pending)\n", + .window_blur => "WindowBlur: 40%\n", .transition => |transition| switch (transition) { .off => unreachable, .slide => "Panel transition: PanelSlide\n", @@ -1160,6 +1199,8 @@ pub const Runtime = struct { const defaults_report = storage[0..out.end]; try std.testing.expect(std.mem.indexOf(u8, defaults_report, "WindowOpacity: unsupported\n") != null); try std.testing.expect(!find("WindowOpacity").?.enabled(defaults_context.capabilities)); + try std.testing.expect(std.mem.indexOf(u8, defaults_report, "WindowBlur: unsupported\n") != null); + try std.testing.expect(!find("WindowBlur").?.enabled(defaults_context.capabilities)); try std.testing.expect(find("WindowOpacity").?.enabled(context.capabilities)); try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell requested (new panes): /bin/sh (default)\n") != null); try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell effective (last spawn): (none)\n") != null); diff --git a/src/macos.zig b/src/macos.zig index bbfb8c08..d0d379de 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -92,6 +92,7 @@ pub const Image = extern struct { iw: u32, ih: u32, rgba: [*]const u8, + paper_bg: u32, }; pub const Runtime = extern struct { @@ -902,6 +903,11 @@ export fn pardes_window_opacity() u8 { return st.core.settings.window_opacity; } +/// Public AppKit backdrop strength, independent of background paint opacity. +export fn pardes_window_blur() u8 { + return if (state) |st| st.core.settings.window_blur else 0; +} + fn taglineFontPercent(core: ?*const pardes.Pardes) u8 { return if (core) |p| p.settings.font.tagline_percent else pardes.config.gui_tagline_font_percent; } @@ -1879,6 +1885,10 @@ fn collectImages(st: *State, surface: *const pardes.Surface) void { .iw = @intCast(place.iw), .ih = @intCast(place.ih), .rgba = place.rgba.ptr, + .paper_bg = if (place.native.paper_bg) |bg| + @as(u32, bg[0]) << 16 | @as(u32, bg[1]) << 8 | bg[2] + else + color_default, }; st.images_len += 1; } @@ -2611,6 +2621,7 @@ test "pardes.h declares every export the way it is defined" { try expectSameAbi(@TypeOf(c.pardes_active_dirty), @TypeOf(pardes_active_dirty)); try expectSameAbi(@TypeOf(c.pardes_theme_bg), @TypeOf(pardes_theme_bg)); try expectSameAbi(@TypeOf(c.pardes_window_opacity), @TypeOf(pardes_window_opacity)); + try expectSameAbi(@TypeOf(c.pardes_window_blur), @TypeOf(pardes_window_blur)); } test "pardes.h matches the Zig boundary" { @@ -2941,3 +2952,21 @@ test "mac tag layer ABI preserves logical capacity and physical grip" { try std.testing.expectEqual(@as(u8, 'G'), canonical[18].text[0]); try std.testing.expectEqual(encodeCell(.{}).len, canonical[10].len); } + +test "mac window blur builtin reaches the ABI without changing opacity" { + try std.testing.expectEqual(@as(u8, 0), pardes_window_blur()); + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 8, .rows = 3 }); + defer core.deinit(); + state = .{ .gpa = gpa, .threaded = undefined, .io = std.testing.io, .core = core, .runtime = .{}, .config_arena = .init(gpa), .prompt_rcs = .{} }; + defer { + state.?.config_arena.deinit(); + state = null; + } + try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 71")); + for ([_][]const u8{ "WindowBlur 0", "WindowBlur 40", "WindowBlur 100", "WindowBlur 101", "WindowBlur 0" }, [_]u8{ 0, 40, 100, 100, 0 }) |command, expected| { + try std.testing.expect(core.executeBuiltinLine(0, command)); + try std.testing.expectEqual(expected, pardes_window_blur()); + try std.testing.expectEqual(@as(u8, 71), pardes_window_opacity()); + } +} diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift index 4e91e407..cbe9cdc9 100644 --- a/src/macos/Sources/AppDelegate.swift +++ b/src/macos/Sources/AppDelegate.swift @@ -81,8 +81,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { // The grid and the blur are SIBLINGS in a plain container, not parent // and child. A transparent theme has to show something through the // grid, and AppKit will not blur what is behind a window unless an - // NSVisualEffectView asks it to — but the backdrop is hidden for every - // theme that brings its own background, and hiding a superview hides + // NSVisualEffectView asks it to. The backdrop can be hidden independently + // when blur is disabled or the ground is opaque; hiding a superview hides // its subviews with it. Nested, an opaque theme drew a blank window. let container = NSView(frame: NSRect(origin: .zero, size: want)) container.autoresizesSubviews = true @@ -460,6 +460,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { /// theme whose background happened to equal the view's starting guess /// would otherwise leave the window in AppKit's default clothes forever. private var dressed = false + private var appliedBlur: UInt8 = 0 private func applyTheme() { // Both reads happen every pump, and neither may be short-circuited by @@ -468,15 +469,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { // tick would leave one of them unworn. let themeChanged = view.adoptThemeBG(pardes_theme_bg()) let opacityChanged = view.adoptWindowOpacity(pardes_window_opacity()) - guard themeChanged || opacityChanged || !dressed else { return } + let blur = pardes_window_blur() + let blurChanged = blur != appliedBlur + guard themeChanged || opacityChanged || blurChanged || !dressed else { return } + appliedBlur = blur + backdrop.alphaValue = CGFloat(blur) / 100 + backdrop.isHidden = blur == 0 || (view.themeBG != nil && view.backgroundAlpha >= 1) dressed = true - // A ground short of solid needs the same non-opaque window a - // background-less theme does — but not the blur behind it. The backdrop - // answers "this theme brings no background"; WindowOpacity says how - // much of the one it brings to paint, and what shows through the rest - // is the desktop, which is what asking for a translucent window means. + // Opacity paints the theme ground; blur controls the native material + // behind it. An opaque ground hides the backdrop without losing the + // requested strength, so lowering opacity restores the effect. if let rgb = view.themeBG { - backdrop.isHidden = true window.isOpaque = view.backgroundAlpha >= 1 window.backgroundColor = view.backgroundAlpha < 1 ? .clear : NSColor( srgbRed: CGFloat((rgb >> 16) & 0xFF) / 255, @@ -493,10 +496,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { window.appearance = NSAppearance(named: luma > 0.5 ? .aqua : .darkAqua) } else { // Transparent: the window stops painting anything of its own and - // the blur takes over. isOpaque false is what lets the desktop + // the optional blur sits behind it. isOpaque false lets the desktop // reach the backdrop at all — a titled window is opaque by default // and would composite over it. - backdrop.isHidden = false window.isOpaque = false window.backgroundColor = .clear window.appearance = NSAppearance(named: .darkAqua) diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index 7c537055..7981cf46 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -1446,6 +1446,7 @@ final class PardesView: NSView { let source: CGRect let body: CGRect let destination: CGRect + let paperBG: UInt32 } /// Rasterized attachments, decoded once each. The bytes the core lends are @@ -1496,7 +1497,7 @@ final class PardesView: NSView { height: CGFloat(place.dst_h) / scale) let frozen = FrozenImage( key: key, image: crop, source: source, - body: body, destination: dst) + body: body, destination: dst, paperBG: place.paper_bg) frameImages.append(frozen) drawFrozenImage(ctx, frozen) } @@ -1545,11 +1546,24 @@ final class PardesView: NSView { ctx.clip(to: placed.body) // isFlipped gives us a y-down CTM and CGImage draws +y up, so flip // about this destination rather than changing the grid arithmetic. + let isPDF = placed.paperBG != UInt32(PARDES_COLOR_DEFAULT) + if isPDF { + fillBackground(ctx, placed.destination, placed.paperBG, backgroundAlpha) + } ctx.translateBy(x: placed.destination.minX, y: placed.destination.maxY) ctx.scaleBy(x: 1, y: -1) ctx.interpolationQuality = .high let rect = CGRect(x: 0, y: 0, width: placed.destination.width, height: placed.destination.height) + if isPDF { + // Paper was painted at WindowOpacity. Content keeps the PDF's own + // coverage, so glyphs, photos and paths stay legible at zero opacity. + ctx.setAlpha(1) + ctx.setBlendMode(.normal) + ctx.draw(placed.image, in: rect) + ctx.restoreGState() + return + } // Match image.frag.glsl/backgroundLayerBlend: source coverage removes // the destination independently of WindowOpacity, then the source is // added at that opacity. Ordinary source-over would compound alpha; diff --git a/src/macos/pardes.h b/src/macos/pardes.h index a2e34f53..df3ba04f 100644 --- a/src/macos/pardes.h +++ b/src/macos/pardes.h @@ -262,6 +262,8 @@ uint32_t pardes_theme_bg(void); // Read it once per pump beside pardes_theme_bg(): reading also acknowledges // whatever `WindowOpacity` asked for. 100 is opaque and is the default. uint8_t pardes_window_opacity(void); +// Backdrop material strength, 0..100; zero disables blur. Not a pixel radius. +uint8_t pardes_window_blur(void); // Pane taglines use this percentage of the current body font size. It is the // embedded default before init and the live core setting afterwards; the host @@ -475,6 +477,8 @@ typedef struct { uint32_t ih; // iw * ih * 4 bytes, RGBA8, borrowed until the next pardes_frame const uint8_t *rgba; + // PDF paper RGB; PARDES_COLOR_DEFAULT means an ordinary image. + uint32_t paper_bg; } pardes_image_s; // This frame's attachments, in paint order. Ask after pardes_frame; both are diff --git a/src/panes.zig b/src/panes.zig index 93abae30..49e5d624 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -4577,13 +4577,14 @@ pub const Pdf = struct { { const tz_render = tracy.zone(@src(), "pdf.render_into"); defer tz_render.end(); - state.document.renderIntoAt( + state.document.renderIntoAtWithPaper( page, request, shape, want, page_highlights, fresh, + pardes.platform == .macos, ) catch break :filled false; } const tz_tint = tracy.zone(@src(), "pdf.tint"); @@ -5852,6 +5853,10 @@ pub const Pdf = struct { .pan_x = placed.pan_x, .geometry = placed.geometry, .pixel_offset_y = placed.pixel_offset_y, + .paper_bg = if (pardes.platform == .macos) + (if (key.mode == .disabled) .{ 255, 255, 255 } else key.colors.background) + else + null, }, .x = text_x, .y = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H, diff --git a/src/pardes.zig b/src/pardes.zig index 94f67ced..5c0ac263 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3593,6 +3593,8 @@ pub const NativePlacement = if (pdf_enabled) struct { geometry: ?image.NativeGeometry = null, /// Subpixel vertical displacement retained from proportional SDL wheel dy. pixel_offset_y: f32 = 0, + /// Separate paper behind straight-alpha PDF content; null for other images. + paper_bg: ?[3]u8 = null, } else struct {}; pub const ImagePlace = struct { diff --git a/src/pdf.zig b/src/pdf.zig index 052d34db..712750dc 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -976,6 +976,20 @@ pub const Document = struct { highlights: []const Highlight, rgba: []u8, ) !void { + return document.renderIntoAtWithPaper(page, request, raster, band, highlights, rgba, false); + } + + /// Preserve the PDF's content coverage when the host paints paper separately. + pub fn renderIntoAtWithPaper( + document: *Document, + page: usize, + request: RenderRequest, + raster: Raster, + band: Raster.Band, + highlights: []const Highlight, + rgba: []u8, + transparent_paper: bool, + ) !void { if (rgba.len != band.len) return error.BadPixmap; if (band.height == 0 or band.y + band.height > raster.height) return error.BadPixmap; @@ -999,6 +1013,7 @@ pub const Document = struct { @intCast(raster.stride), @intCast(band.y), @intCast(band.height), + @intFromBool(transparent_paper), ) != c.PARDES_PDF_OK) return error.RenderFailed; } @@ -1832,6 +1847,7 @@ test "RGBA allocation and buffer-validation failures leave the document renderab raw_layout.stride, 0, raw_layout.height, + 0, )); for (scratch) |byte| try std.testing.expectEqual(@as(u8, 0xa5), byte); @@ -2074,3 +2090,35 @@ test "offset crop and rotation keep normalized highlights pixel-aligned" { marked.rgba[left + 1] < 10 and marked.rgba[left + 2] > 240); try std.testing.expectEqualSlices(u8, plain.rgba[right .. right + 4], marked.rgba[right .. right + 4]); } + +test "transparent PDF paper preserves ink and band coverage" { + var document = try Document.open("docs/9p.pdf"); + defer document.deinit(); + const request: RenderRequest = .{ .dpi = 96, .max_dimension = 1200 }; + const raster = try document.measureRenderAt(0, request); + const flattened = try std.testing.allocator.alloc(u8, raster.len); + defer std.testing.allocator.free(flattened); + const content = try std.testing.allocator.alloc(u8, raster.len); + defer std.testing.allocator.free(content); + try document.renderIntoAt(0, request, raster, raster.wholePage(), &.{}, flattened); + try document.renderIntoAtWithPaper(0, request, raster, raster.wholePage(), &.{}, content, true); + var paper: usize = 0; + var ink: usize = 0; + var edges: usize = 0; + for (std.mem.bytesAsSlice([4]u8, content), std.mem.bytesAsSlice([4]u8, flattened)) |pixel, reference| { + const a: u32 = pixel[3]; + paper += @intFromBool(a == 0); + ink += @intFromBool(a == 255); + edges += @intFromBool(a > 0 and a < 255); + for (0..3) |channel| { + const over_white = (a * pixel[channel] + (255 - a) * 255 + 127) / 255; + try std.testing.expect(@abs(@as(i32, @intCast(over_white)) - @as(i32, reference[channel])) <= 2); + } + } + try std.testing.expect(paper > 100 and ink > 100 and edges > 100); + const band = raster.band(raster.height / 3, raster.height / 4); + const rows = try std.testing.allocator.alloc(u8, band.len); + defer std.testing.allocator.free(rows); + try document.renderIntoAtWithPaper(0, request, raster, band, &.{}, rows, true); + try std.testing.expectEqualSlices(u8, content[band.y * raster.stride ..][0..band.len], rows); +} diff --git a/src/pdf_bridge.c b/src/pdf_bridge.c index bdc7b891..976ed838 100644 --- a/src/pdf_bridge.c +++ b/src/pdf_bridge.c @@ -588,7 +588,8 @@ pardes_pdf_render_into( int height, int stride, int band_y, - int band_height) + int band_height, + int transparent_paper) { fz_context *ctx; fz_pixmap *pixmap = NULL; @@ -654,7 +655,8 @@ pardes_pdf_render_into( * paid either way by the glyph spans and the tint pass that walk the * same buffer immediately afterwards. Leave it alone. */ - fz_clear_pixmap_with_value(ctx, pixmap, 0xFF); + if (transparent_paper) fz_clear_pixmap(ctx, pixmap); + else fz_clear_pixmap_with_value(ctx, pixmap, 0xFF); if (document->cached_display_list != NULL || document->display_list_candidate_page_number == page_number) { @@ -719,6 +721,21 @@ pardes_pdf_render_into( return PARDES_PDF_ERROR; } + /* MuPDF draws premultiplied samples. The public RGBA contract and tint + * pass use straight alpha; preserve coverage and undo premultiplication. */ + if (transparent_paper) { + size_t pixel; + for (pixel = 0; pixel < samples_len; pixel += 4) { + unsigned int a = samples[pixel + 3]; + int channel; + if (a == 0 || a == 255) continue; + for (channel = 0; channel < 3; ++channel) { + unsigned int straight = (samples[pixel + channel] * 255u + a / 2) / a; + samples[pixel + channel] = (unsigned char)(straight > 255 ? 255 : straight); + } + } + } + if (document->cached_display_list == NULL) document->display_list_candidate_page_number = page_number; diff --git a/src/pdf_bridge.h b/src/pdf_bridge.h index 8c73f688..cda426aa 100644 --- a/src/pdf_bridge.h +++ b/src/pdf_bridge.h @@ -220,7 +220,8 @@ int pardes_pdf_measure_render( * A full page is the band `(0, height)`. * * MuPDF wraps but never owns or frees samples. Every successful pixel is - * opaque, including the caller-supplied highlights composited onto the page. + * opaque unless transparent_paper is nonzero. That mode preserves content + * coverage and returns straight-alpha RGBA over an unpainted page. * The CTM does not depend on the band, so a band's rows are bit-identical to * the same rows of the whole page: it is a clip, not a different rendering. */ @@ -239,7 +240,8 @@ int pardes_pdf_render_into( int height, int stride, int band_y, - int band_height + int band_height, + int transparent_paper ); int pardes_pdf_page_text( |
