mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-14 15:03:27 -04:00
screenshot: simplify the rasterizer
Move the ICU bindings to src/sys/icu.zig, read the face metrics from the font tables instead of hardcoding DejaVu's, use the zlib header through translate-c rather than hand-declared externs, encode the PNG straight from the surface strip, and share the build's dev-optimize rule between z2d and zlib. Render output is unchanged.
This commit is contained in:
1 parent
e10de2f602
commit
5ce92ffa6f
6 files changed
+660
-691
No files matched your search
@@ -105,11 +105,10 @@ pub fn build(b: *Build) !void {
|
||||
lightpanda_module.addImport("lightpanda", lightpanda_module); // allow circular "lightpanda" import
|
||||
lightpanda_module.addImport("build_config", opts.createModule());
|
||||
|
||||
// Rasterization is hot in screenshot/raster.zig; keep z2d optimized in
|
||||
// dev builds, like the Rust workspace does for its dependencies.
|
||||
// Rasterization is hot in screenshot/raster.zig.
|
||||
const z2d_dep = b.dependency("z2d", .{
|
||||
.target = target,
|
||||
.optimize = if (optimize == .Debug) .ReleaseFast else optimize,
|
||||
.optimize = depOptimize(optimize),
|
||||
});
|
||||
lightpanda_module.addImport("z2d", z2d_dep.module("z2d"));
|
||||
|
||||
@@ -443,9 +442,15 @@ fn linkCurl(b: *Build, mod: *Build.Module, is_tsan: bool) void {
|
||||
mod.addImport("curl", translate_c.createModule());
|
||||
|
||||
// Deflate is on the screenshot hot path; -O0 zlib is ~10x slower.
|
||||
const zlib_optimize: std.builtin.OptimizeMode = if (mod.optimize.? == .Debug) .ReleaseFast else mod.optimize.?;
|
||||
const zlib = buildZlib(b, target, zlib_optimize, is_tsan);
|
||||
const zlib = buildZlib(b, target, depOptimize(mod.optimize.?), is_tsan);
|
||||
curl.root_module.linkLibrary(zlib);
|
||||
mod.linkLibrary(zlib);
|
||||
const zlib_c = b.addTranslateC(.{
|
||||
.root_source_file = b.dependency("zlib", .{}).path("zlib.h"),
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
});
|
||||
mod.addImport("zlib", zlib_c.createModule());
|
||||
|
||||
const brotli = buildBrotli(b, target, mod.optimize.?, is_tsan);
|
||||
for (brotli) |lib| curl.root_module.linkLibrary(lib);
|
||||
@@ -473,6 +478,12 @@ fn cLibModule(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opt
|
||||
});
|
||||
}
|
||||
|
||||
/// Dependencies on hot paths stay optimized in dev builds; their own debug
|
||||
/// checks are not what a Debug build of lightpanda is for.
|
||||
fn depOptimize(optimize: std.builtin.OptimizeMode) std.builtin.OptimizeMode {
|
||||
return if (optimize == .Debug) .ReleaseFast else optimize;
|
||||
}
|
||||
|
||||
fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Step.Compile {
|
||||
const dep = b.dependency("zlib", .{});
|
||||
|
||||
|
||||
+109
-160
@@ -43,8 +43,7 @@ pub fn png(arena: Allocator, node: *Node, opts: Opts, writer: *std.Io.Writer, fr
|
||||
// get the height of the PNG if we were to render it.
|
||||
pub fn contentHeight(arena: Allocator, node: *Node, width: u32, frame: *Frame) !u32 {
|
||||
const prepared = try prepare(arena, node, .{ .width = width }, frame);
|
||||
var discard: std.Io.Writer.Discarding = .init(&.{});
|
||||
return prepared.stream(&discard.writer, true);
|
||||
return prepared.stream(null);
|
||||
}
|
||||
|
||||
// The DOM walk, done up front so it can fail (allocation) before any output
|
||||
@@ -69,11 +68,12 @@ pub const Prepared = struct {
|
||||
blocks: []const raster.Block,
|
||||
|
||||
pub fn write(self: *const Prepared, writer: *std.Io.Writer) std.Io.Writer.Error!u32 {
|
||||
return self.stream(writer, false);
|
||||
return self.stream(writer);
|
||||
}
|
||||
|
||||
fn stream(self: *const Prepared, writer: *std.Io.Writer, measure_only: bool) std.Io.Writer.Error!u32 {
|
||||
return raster.run(self.arena, self.blocks, self.opts, writer, measure_only) catch |err| switch (err) {
|
||||
// A null writer only measures.
|
||||
fn stream(self: *const Prepared, writer: ?*std.Io.Writer) std.Io.Writer.Error!u32 {
|
||||
return raster.run(self.arena, self.blocks, self.opts, writer) catch |err| switch (err) {
|
||||
// The layout pass fails before any output starts, so this can't
|
||||
// hand back a truncated PNG. WriteFailed is the only error
|
||||
// jsonStringify's signature can carry, hence the log line.
|
||||
@@ -98,15 +98,6 @@ pub const Prepared = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const SPAN_BOLD = raster.SPAN_BOLD;
|
||||
const SPAN_ITALIC = raster.SPAN_ITALIC;
|
||||
const SPAN_UNDERLINE = raster.SPAN_UNDERLINE;
|
||||
const SPAN_MONO = raster.SPAN_MONO;
|
||||
const SPAN_STRIKE = raster.SPAN_STRIKE;
|
||||
const SPAN_HAS_COLOR = raster.SPAN_HAS_COLOR;
|
||||
|
||||
const BLOCK_TIGHT = raster.BLOCK_TIGHT;
|
||||
|
||||
const LINK_COLOR: u32 = 0x1a0dab;
|
||||
const MUTED_COLOR: u32 = 0x6b6b6b;
|
||||
|
||||
@@ -126,7 +117,6 @@ const Builder = struct {
|
||||
text: std.ArrayList(u8) = .empty,
|
||||
text_flags: u32 = 0,
|
||||
text_color: u32 = 0,
|
||||
has_content: bool = false,
|
||||
pending_space: bool = false,
|
||||
// Nothing appended since an <a> closed. Adjacent links with no whitespace
|
||||
// between them (nav bars) would otherwise fuse into one word.
|
||||
@@ -166,22 +156,22 @@ const Builder = struct {
|
||||
fn currentFlags(self: *const Builder) u32 {
|
||||
var flags: u32 = 0;
|
||||
if (self.bold > 0) {
|
||||
flags |= SPAN_BOLD;
|
||||
flags |= raster.SPAN_BOLD;
|
||||
}
|
||||
if (self.italic > 0) {
|
||||
flags |= SPAN_ITALIC;
|
||||
flags |= raster.SPAN_ITALIC;
|
||||
}
|
||||
if (self.underline > 0 or self.link > 0) {
|
||||
flags |= SPAN_UNDERLINE;
|
||||
flags |= raster.SPAN_UNDERLINE;
|
||||
}
|
||||
if (self.mono > 0) {
|
||||
flags |= SPAN_MONO;
|
||||
flags |= raster.SPAN_MONO;
|
||||
}
|
||||
if (self.strike > 0) {
|
||||
flags |= SPAN_STRIKE;
|
||||
flags |= raster.SPAN_STRIKE;
|
||||
}
|
||||
if (self.link > 0 or self.muted > 0) {
|
||||
flags |= SPAN_HAS_COLOR;
|
||||
flags |= raster.SPAN_HAS_COLOR;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
@@ -196,6 +186,10 @@ const Builder = struct {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn hasContent(self: *const Builder) bool {
|
||||
return self.spans.items.len > 0 or self.text.items.len > 0;
|
||||
}
|
||||
|
||||
fn openBlock(self: *Builder, kind: raster.Block.Kind, level: u8) Error!void {
|
||||
try self.closeBlock();
|
||||
self.block_open = true;
|
||||
@@ -234,7 +228,7 @@ const Builder = struct {
|
||||
.level = self.level,
|
||||
.list_depth = self.list_depth,
|
||||
.quote_depth = self.quote_depth,
|
||||
.flags = if (self.tight > 0) BLOCK_TIGHT else 0,
|
||||
.flags = if (self.tight > 0) raster.BLOCK_TIGHT else 0,
|
||||
});
|
||||
} else if (self.marker.len > 0) {
|
||||
// Empty <li>: keep the marker for whatever block comes next.
|
||||
@@ -243,7 +237,6 @@ const Builder = struct {
|
||||
self.block_open = false;
|
||||
self.marker = "";
|
||||
self.spans.clearRetainingCapacity();
|
||||
self.has_content = false;
|
||||
self.pending_space = false;
|
||||
self.after_anchor = false;
|
||||
}
|
||||
@@ -259,12 +252,11 @@ const Builder = struct {
|
||||
self.text_flags = flags;
|
||||
self.text_color = color;
|
||||
try self.text.appendSlice(self.arena, text);
|
||||
self.has_content = true;
|
||||
self.after_anchor = false;
|
||||
}
|
||||
|
||||
fn appendWord(self: *Builder, word: []const u8) Error!void {
|
||||
if (self.pending_space and self.has_content) {
|
||||
if (self.pending_space and self.hasContent()) {
|
||||
try self.appendSpace();
|
||||
}
|
||||
self.pending_space = false;
|
||||
@@ -276,10 +268,10 @@ const Builder = struct {
|
||||
fn appendSpace(self: *Builder) Error!void {
|
||||
var flags = self.text_flags & self.currentFlags();
|
||||
var color = self.text_color;
|
||||
if (flags & SPAN_HAS_COLOR != 0 and color != self.currentColor()) {
|
||||
flags &= ~SPAN_HAS_COLOR;
|
||||
if (flags & raster.SPAN_HAS_COLOR != 0 and color != self.currentColor()) {
|
||||
flags &= ~raster.SPAN_HAS_COLOR;
|
||||
}
|
||||
if (flags & SPAN_HAS_COLOR == 0) color = 0;
|
||||
if (flags & raster.SPAN_HAS_COLOR == 0) color = 0;
|
||||
|
||||
if (flags != self.text_flags or color != self.text_color) {
|
||||
try self.flushSpan();
|
||||
@@ -391,8 +383,9 @@ const Builder = struct {
|
||||
},
|
||||
.br => {
|
||||
if (self.pre_node != null) return self.append("\n");
|
||||
if (!self.has_content) return;
|
||||
// A hard break within the block: parley honors '\n'.
|
||||
if (!self.hasContent()) return;
|
||||
// A hard break within the block: the wrap treats '\n' as a
|
||||
// mandatory break.
|
||||
try self.append("\n");
|
||||
self.pending_space = false;
|
||||
return;
|
||||
@@ -486,7 +479,7 @@ const Builder = struct {
|
||||
},
|
||||
.slot => return self.renderSlotContent(el.as(Slot)),
|
||||
.td, .th => {
|
||||
if (self.has_content) {
|
||||
if (self.hasContent()) {
|
||||
self.pending_space = true;
|
||||
self.muted += 1;
|
||||
try self.appendWord("|");
|
||||
@@ -542,120 +535,129 @@ const Builder = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
// A frame with `html` parsed under a div; the div is the node to render.
|
||||
fn testRoot(html: []const u8) !struct { frame: *Frame, node: *Node } {
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const div = try frame.window._document.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
|
||||
return .{ .frame = frame, .node = div.asNode() };
|
||||
}
|
||||
|
||||
fn testBlocks(html: []const u8) ![]const raster.Block {
|
||||
const root = try testRoot(html);
|
||||
var builder: Builder = .{ .arena = testing.arena_allocator, .frame = root.frame };
|
||||
try builder.render(root.node);
|
||||
try builder.closeBlock();
|
||||
return builder.blocks.items;
|
||||
}
|
||||
|
||||
fn spanText(block: raster.Block) ![]const u8 {
|
||||
var out: std.ArrayList(u8) = .empty;
|
||||
for (block.spans) |s| try out.appendSlice(testing.arena_allocator, s.text);
|
||||
return out.items;
|
||||
}
|
||||
|
||||
fn testPng(html: []const u8, width: u32) ![]const u8 {
|
||||
const root = try testRoot(html);
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
_ = try png(testing.arena_allocator, root.node, .{ .width = width }, &aw.writer, root.frame);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
/// (width, height) from the IHDR chunk.
|
||||
fn pngSize(data: []const u8) struct { u32, u32 } {
|
||||
return .{ std.mem.readInt(u32, data[16..20], .big), std.mem.readInt(u32, data[20..24], .big) };
|
||||
}
|
||||
|
||||
test "browser.screenshot: png signature and dimensions" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const out = try testPng("<h1>Title</h1><p>Hello <b>world</b> <a href='/x'>link</a></p>", 640);
|
||||
|
||||
try testing.expectEqual(true, out.len > 100);
|
||||
try testing.expectEqual("\x89PNG\r\n\x1a\n", out[0..8]);
|
||||
// IHDR width/height are big-endian at offsets 16 and 20.
|
||||
try testing.expectEqual(640, std.mem.readInt(u32, out[16..20], .big));
|
||||
const height = std.mem.readInt(u32, out[20..24], .big);
|
||||
const width, const height = pngSize(out);
|
||||
try testing.expectEqual(640, width);
|
||||
// Two blocks plus margins.
|
||||
try testing.expectEqual(true, height > 60 and height < 200);
|
||||
}
|
||||
|
||||
test "browser.screenshot: fixed height, clip and scale" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>one</p><p>two</p><p>three</p>");
|
||||
const root = try testRoot("<p>one</p><p>two</p><p>three</p>");
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
const content_height = try png(testing.arena_allocator, div.asNode(), .{ .width = 300, .height = 50, .scale = 2.0 }, &aw.writer, frame);
|
||||
const content_height = try png(testing.arena_allocator, root.node, .{ .width = 300, .height = 50, .scale = 2.0 }, &aw.writer, root.frame);
|
||||
try testing.expectEqual(true, content_height > 50);
|
||||
try testing.expectEqual(600, std.mem.readInt(u32, aw.written()[16..20], .big));
|
||||
try testing.expectEqual(100, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
try testing.expectEqual(.{ 600, 100 }, pngSize(aw.written()));
|
||||
|
||||
try testing.expectEqual(content_height, try contentHeight(frame.call_arena, div.asNode(), 300, frame));
|
||||
try testing.expectEqual(content_height, try contentHeight(root.frame.call_arena, root.node, 300, root.frame));
|
||||
|
||||
aw.clearRetainingCapacity();
|
||||
_ = try png(frame.call_arena, div.asNode(), .{
|
||||
_ = try png(root.frame.call_arena, root.node, .{
|
||||
.width = 300,
|
||||
.clip = .{ .x = 10, .y = 10, .width = 100, .height = 40 },
|
||||
}, &aw.writer, frame);
|
||||
try testing.expectEqual(100, std.mem.readInt(u32, aw.written()[16..20], .big));
|
||||
try testing.expectEqual(40, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
}, &aw.writer, root.frame);
|
||||
try testing.expectEqual(.{ 100, 40 }, pngSize(aw.written()));
|
||||
}
|
||||
|
||||
test "browser.screenshot: a clip past the viewport extends the strip" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>one</p><p>two</p><p>three</p><p>four</p><p>five</p><p>six</p>");
|
||||
const root = try testRoot("<p>one</p><p>two</p><p>three</p><p>four</p><p>five</p><p>six</p>");
|
||||
|
||||
const full = try contentHeight(testing.arena_allocator, div.asNode(), 300, frame);
|
||||
const full = try contentHeight(testing.arena_allocator, root.node, 300, root.frame);
|
||||
try testing.expectEqual(true, full > 100);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
_ = try png(frame.call_arena, div.asNode(), .{
|
||||
_ = try png(root.frame.call_arena, root.node, .{
|
||||
.width = 300,
|
||||
.height = 100,
|
||||
.clip = .{ .x = 0, .y = 0, .width = 300, .height = @floatFromInt(full) },
|
||||
}, &aw.writer, frame);
|
||||
try testing.expectEqual(300, std.mem.readInt(u32, aw.written()[16..20], .big));
|
||||
try testing.expectEqual(full, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
}, &aw.writer, root.frame);
|
||||
try testing.expectEqual(.{ 300, full }, pngSize(aw.written()));
|
||||
|
||||
// Never past the content, so an absurd probe resolves to the full page
|
||||
// instead of a 1e8-tall raster.
|
||||
aw.clearRetainingCapacity();
|
||||
_ = try png(testing.arena_allocator, div.asNode(), .{
|
||||
_ = try png(testing.arena_allocator, root.node, .{
|
||||
.width = 300,
|
||||
.height = 100,
|
||||
.clip = .{ .x = 0, .y = 0, .width = 300, .height = 1e8 },
|
||||
}, &aw.writer, frame);
|
||||
try testing.expectEqual(full, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
}, &aw.writer, root.frame);
|
||||
_, const height = pngSize(aw.written());
|
||||
try testing.expectEqual(full, height);
|
||||
}
|
||||
|
||||
test "browser.screenshot: raster is bounded" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>hello</p>");
|
||||
const root = try testRoot("<p>hello</p>");
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
|
||||
_ = try png(testing.arena_allocator, div.asNode(), .{ .width = 300000, .height = 8 }, &aw.writer, frame);
|
||||
try testing.expectEqual(16384, std.mem.readInt(u32, aw.written()[16..20], .big));
|
||||
try testing.expectEqual(8, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
_ = try png(testing.arena_allocator, root.node, .{ .width = 300000, .height = 8 }, &aw.writer, root.frame);
|
||||
try testing.expectEqual(.{ 16384, 8 }, pngSize(aw.written()));
|
||||
|
||||
aw.clearRetainingCapacity();
|
||||
_ = try png(testing.arena_allocator, div.asNode(), .{ .width = 8, .height = 100000 }, &aw.writer, frame);
|
||||
try testing.expectEqual(8, std.mem.readInt(u32, aw.written()[16..20], .big));
|
||||
try testing.expectEqual(16384, std.mem.readInt(u32, aw.written()[20..24], .big));
|
||||
_ = try png(testing.arena_allocator, root.node, .{ .width = 8, .height = 100000 }, &aw.writer, root.frame);
|
||||
try testing.expectEqual(.{ 8, 16384 }, pngSize(aw.written()));
|
||||
}
|
||||
|
||||
test "browser.screenshot: a refused write fails the capture" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
testing.silenceLog(&.{.browser});
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>hello</p>");
|
||||
const root = try testRoot("<p>hello</p>");
|
||||
|
||||
// Far too small for the PNG, so the sink refuses partway through. That
|
||||
// has to surface as an error and not as a truncated image.
|
||||
var buf: [64]u8 = undefined;
|
||||
var w = std.Io.Writer.fixed(&buf);
|
||||
try testing.expectError(error.WriteFailed, png(testing.arena_allocator, div.asNode(), .{ .width = 300 }, &w, frame));
|
||||
try testing.expectError(error.WriteFailed, png(testing.arena_allocator, root.node, .{ .width = 300 }, &w, root.frame));
|
||||
}
|
||||
|
||||
test "browser.screenshot: json streams base64" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>hello</p>");
|
||||
|
||||
const prepared = try prepare(testing.arena_allocator, div.asNode(), .{ .width = 200 }, frame);
|
||||
const root = try testRoot("<p>hello</p>");
|
||||
const prepared = try prepare(testing.arena_allocator, root.node, .{ .width = 200 }, root.frame);
|
||||
|
||||
var raw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer raw.deinit();
|
||||
@@ -676,11 +678,7 @@ test "browser.screenshot: json streams base64" {
|
||||
|
||||
test "browser.screenshot: block extraction" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(),
|
||||
const blocks = try testBlocks(
|
||||
\\<h2> Head ing </h2>
|
||||
\\<p>Some <b>bold <i>both</i></b> text <a href="/l">a link</a><span> tail</span></p>
|
||||
\\<ul><li>one</li><li><p>two</p><ol><li>nested</li></ol></li></ul>
|
||||
@@ -692,110 +690,75 @@ test "browser.screenshot: block extraction" {
|
||||
\\<table><tr><th>A</th><td>B</td></tr><tr><td>C</td></tr></table>
|
||||
\\<div> </div>
|
||||
);
|
||||
|
||||
var builder: Builder = .{ .arena = testing.arena_allocator, .frame = frame };
|
||||
try builder.render(div.asNode());
|
||||
try builder.closeBlock();
|
||||
|
||||
const blocks = builder.blocks.items;
|
||||
try testing.expectEqual(11, blocks.len);
|
||||
|
||||
const S = struct {
|
||||
fn text(b: raster.Block, arena: Allocator) ![]const u8 {
|
||||
var out: std.ArrayList(u8) = .empty;
|
||||
for (b.spans) |s| try out.appendSlice(arena, s.text);
|
||||
return out.items;
|
||||
}
|
||||
};
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
try testing.expectEqual(.heading, blocks[0].kind);
|
||||
try testing.expectEqual(2, blocks[0].level);
|
||||
try testing.expectEqual("Head ing", try S.text(blocks[0], arena));
|
||||
try testing.expectEqual("Head ing", try spanText(blocks[0]));
|
||||
|
||||
try testing.expectEqual(.paragraph, blocks[1].kind);
|
||||
try testing.expectEqual("Some bold both text a link tail", try S.text(blocks[1], arena));
|
||||
try testing.expectEqual("Some bold both text a link tail", try spanText(blocks[1]));
|
||||
try testing.expectEqual(6, blocks[1].spans.len);
|
||||
try testing.expectEqual("Some ", blocks[1].spans[0].text);
|
||||
try testing.expectEqual(SPAN_BOLD, blocks[1].spans[1].flags);
|
||||
try testing.expectEqual(raster.SPAN_BOLD, blocks[1].spans[1].flags);
|
||||
try testing.expectEqual("bold ", blocks[1].spans[1].text);
|
||||
try testing.expectEqual(SPAN_BOLD | SPAN_ITALIC, blocks[1].spans[2].flags);
|
||||
try testing.expectEqual(raster.SPAN_BOLD | raster.SPAN_ITALIC, blocks[1].spans[2].flags);
|
||||
try testing.expectEqual("both", blocks[1].spans[2].text);
|
||||
try testing.expectEqual(0, blocks[1].spans[3].flags);
|
||||
try testing.expectEqual(" text ", blocks[1].spans[3].text);
|
||||
try testing.expectEqual(SPAN_UNDERLINE | SPAN_HAS_COLOR, blocks[1].spans[4].flags);
|
||||
try testing.expectEqual(raster.SPAN_UNDERLINE | raster.SPAN_HAS_COLOR, blocks[1].spans[4].flags);
|
||||
try testing.expectEqual(LINK_COLOR, blocks[1].spans[4].color);
|
||||
try testing.expectEqual("a link", blocks[1].spans[4].text);
|
||||
try testing.expectEqual(" tail", blocks[1].spans[5].text);
|
||||
|
||||
try testing.expectEqual("one", try S.text(blocks[2], arena));
|
||||
try testing.expectEqual("one", try spanText(blocks[2]));
|
||||
try testing.expectEqual(1, blocks[2].list_depth);
|
||||
try testing.expectEqual("•", blocks[2].marker);
|
||||
try testing.expectEqual("two", try S.text(blocks[3], arena));
|
||||
try testing.expectEqual("two", try spanText(blocks[3]));
|
||||
try testing.expectEqual("•", blocks[3].marker);
|
||||
try testing.expectEqual("nested", try S.text(blocks[4], arena));
|
||||
try testing.expectEqual("nested", try spanText(blocks[4]));
|
||||
try testing.expectEqual(2, blocks[4].list_depth);
|
||||
try testing.expectEqual("1.", blocks[4].marker);
|
||||
|
||||
try testing.expectEqual("quote", try S.text(blocks[5], arena));
|
||||
try testing.expectEqual("quote", try spanText(blocks[5]));
|
||||
try testing.expectEqual(1, blocks[5].quote_depth);
|
||||
try testing.expectEqual(0, blocks[5].list_depth);
|
||||
|
||||
try testing.expectEqual(.pre, blocks[6].kind);
|
||||
try testing.expectEqual(" keep\n this", try S.text(blocks[6], arena));
|
||||
try testing.expectEqual(" keep\n this", try spanText(blocks[6]));
|
||||
try testing.expectEqual(.rule, blocks[7].kind);
|
||||
try testing.expectEqual("a picture", try S.text(blocks[8], arena));
|
||||
try testing.expectEqual(SPAN_ITALIC | SPAN_HAS_COLOR, blocks[8].spans[0].flags);
|
||||
try testing.expectEqual("A | B", try S.text(blocks[9], arena));
|
||||
try testing.expectEqual(SPAN_BOLD, blocks[9].spans[0].flags);
|
||||
try testing.expectEqual("C", try S.text(blocks[10], arena));
|
||||
try testing.expectEqual("a picture", try spanText(blocks[8]));
|
||||
try testing.expectEqual(raster.SPAN_ITALIC | raster.SPAN_HAS_COLOR, blocks[8].spans[0].flags);
|
||||
try testing.expectEqual("A | B", try spanText(blocks[9]));
|
||||
try testing.expectEqual(raster.SPAN_BOLD, blocks[9].spans[0].flags);
|
||||
try testing.expectEqual("C", try spanText(blocks[10]));
|
||||
}
|
||||
|
||||
test "browser.screenshot: adjacent anchors" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
// Inside <p> (not a layout block) so markdown's standalone rule doesn't
|
||||
// apply and the anchors flow inline.
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(),
|
||||
const blocks = try testBlocks(
|
||||
\\<p><a href="/a">Log In</a><a href="/b">Sign Up</a><b>!</b> see <a href="/c">this</a>.</p>
|
||||
);
|
||||
|
||||
var builder: Builder = .{ .arena = testing.arena_allocator, .frame = frame };
|
||||
try builder.render(div.asNode());
|
||||
try builder.closeBlock();
|
||||
const blocks = builder.blocks.items;
|
||||
try testing.expectEqual(1, blocks.len);
|
||||
var out: std.ArrayList(u8) = .empty;
|
||||
for (blocks[0].spans) |sp| try out.appendSlice(testing.arena_allocator, sp.text);
|
||||
try testing.expectString("Log In Sign Up! see this.", out.items);
|
||||
try testing.expectString("Log In Sign Up! see this.", try spanText(blocks[0]));
|
||||
}
|
||||
|
||||
test "browser.screenshot: standalone anchors get their own block" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(),
|
||||
const blocks = try testBlocks(
|
||||
\\<nav><a href="/a">bsky</a><a href="/b">rss</a></nav>
|
||||
\\<div><a href="/p"><h3>Post title</h3><span>Aug 04</span></a></div>
|
||||
\\<p>inline <a href="/x">link</a> here</p>
|
||||
);
|
||||
|
||||
var builder: Builder = .{ .arena = testing.arena_allocator, .frame = frame };
|
||||
try builder.render(div.asNode());
|
||||
try builder.closeBlock();
|
||||
const blocks = builder.blocks.items;
|
||||
try testing.expectEqual(5, blocks.len);
|
||||
try testing.expectString("bsky", blocks[0].spans[0].text);
|
||||
try testing.expectEqual(BLOCK_TIGHT, blocks[0].flags);
|
||||
try testing.expectEqual(raster.BLOCK_TIGHT, blocks[0].flags);
|
||||
try testing.expectString("rss", blocks[1].spans[0].text);
|
||||
try testing.expectEqual(.heading, blocks[2].kind);
|
||||
try testing.expectString("Post title", blocks[2].spans[0].text);
|
||||
try testing.expectEqual(SPAN_UNDERLINE | SPAN_HAS_COLOR, blocks[2].spans[0].flags);
|
||||
try testing.expectEqual(raster.SPAN_UNDERLINE | raster.SPAN_HAS_COLOR, blocks[2].spans[0].flags);
|
||||
try testing.expectString("Aug 04", blocks[3].spans[0].text);
|
||||
try testing.expectEqual(3, blocks[4].spans.len);
|
||||
try testing.expectEqual(0, blocks[4].flags);
|
||||
@@ -805,8 +768,7 @@ test "browser.screenshot: shadow dom and slots" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
const div = try frame.window._document.createElement("div", null, frame);
|
||||
try div.setHTMLUnsafe(
|
||||
\\<x-host><template shadowrootmode="open"><p>shadow <slot></slot></p></template>light</x-host>
|
||||
, frame);
|
||||
@@ -827,16 +789,3 @@ test "browser.screenshot: stacked marks on a later line" {
|
||||
const out = try testPng("<p>first line<br>\xd8\xb4\xd9\x8e\xd8\xaf\xd9\x91\xd9\x8e\xd8\xa9 caf\xc3\xa9 e\xcc\x81</p>", 400);
|
||||
try testing.expectEqual("\x89PNG\r\n\x1a\n", out[0..8]);
|
||||
}
|
||||
|
||||
fn testPng(html: []const u8, width: u32) ![]const u8 {
|
||||
const frame = try testing.createFrame();
|
||||
frame.url = "http://localhost/";
|
||||
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
_ = try png(testing.arena_allocator, div.asNode(), .{ .width = width }, &aw.writer, frame);
|
||||
return aw.written();
|
||||
}
|
||||
@@ -17,10 +17,10 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Reader for the OpenType GPOS mark-attachment lookups (mark-to-base,
|
||||
//! mark-to-ligature, mark-to-mark) and GDEF glyph classes, straight off the
|
||||
//! font bytes. Lookups are gathered from the mark/mkmk features of every
|
||||
//! script and applied in lookup-index order; the first covering subtable
|
||||
//! wins. LookupFlag filtering is not applied.
|
||||
//! mark-to-ligature, mark-to-mark), GDEF glyph classes and the vertical
|
||||
//! metrics tables, straight off the font bytes. Lookups are gathered from
|
||||
//! the mark/mkmk features of every script and applied in lookup-index order;
|
||||
//! the first covering subtable wins. LookupFlag filtering is not applied.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -47,6 +47,33 @@ pub const Offset = struct {
|
||||
|
||||
pub const Error = error{ MalformedFont, OutOfMemory };
|
||||
|
||||
/// Font units, as the tables store them (descent is negative).
|
||||
pub const Metrics = struct {
|
||||
units_per_em: u16,
|
||||
ascent: i16,
|
||||
descent: i16,
|
||||
underline_position: i16,
|
||||
underline_thickness: i16,
|
||||
strikeout_position: i16,
|
||||
strikeout_size: i16,
|
||||
};
|
||||
|
||||
pub fn readMetrics(self: *const Gpos) Error!Metrics {
|
||||
const head = try self.findTable("head") orelse return error.MalformedFont;
|
||||
const hhea = try self.findTable("hhea") orelse return error.MalformedFont;
|
||||
const post = try self.findTable("post") orelse return error.MalformedFont;
|
||||
const os2 = try self.findTable("OS/2") orelse return error.MalformedFont;
|
||||
return .{
|
||||
.units_per_em = try self.intAt(u16, head + 18),
|
||||
.ascent = try self.intAt(i16, hhea + 4),
|
||||
.descent = try self.intAt(i16, hhea + 6),
|
||||
.underline_position = try self.intAt(i16, post + 8),
|
||||
.underline_thickness = try self.intAt(i16, post + 10),
|
||||
.strikeout_size = try self.intAt(i16, os2 + 26),
|
||||
.strikeout_position = try self.intAt(i16, os2 + 28),
|
||||
};
|
||||
}
|
||||
|
||||
const MARK_CLASS: u16 = 3;
|
||||
|
||||
pub fn init(allocator: Allocator, data: []const u8) Error!Gpos {
|
||||
@@ -150,25 +177,24 @@ fn attach(self: *const Gpos, parent: u16, mark: u16, parent_is_mark: bool) Error
|
||||
if (class >= class_count) return error.MalformedFont;
|
||||
const mark_anchor = mark_array + try self.u16At(mark_record + 2);
|
||||
|
||||
// Anchor offsets are relative to the array holding them: the
|
||||
// BaseArray/Mark2Array, or the ligature's own attach table.
|
||||
const record_size = @as(usize, class_count) * 2;
|
||||
const parent_anchor_off = switch (lookup.kind) {
|
||||
.mark_base, .mark_mark => try self.u16At(parent_array + 2 + @as(usize, pi) * record_size + class * 2),
|
||||
const anchors_base, const anchor_off = switch (lookup.kind) {
|
||||
.mark_base, .mark_mark => .{ parent_array, try self.u16At(parent_array + 2 + @as(usize, pi) * record_size + class * 2) },
|
||||
.mark_lig => blk: {
|
||||
const lig_attach = parent_array + try self.u16At(parent_array + 2 + @as(usize, pi) * 2);
|
||||
const component_count = try self.u16At(lig_attach);
|
||||
if (component_count == 0) break :blk 0;
|
||||
break :blk try self.u16At(lig_attach + 2 + (component_count - 1) * record_size + class * 2);
|
||||
if (component_count == 0) break :blk .{ lig_attach, 0 };
|
||||
break :blk .{ lig_attach, try self.u16At(lig_attach + 2 + (component_count - 1) * record_size + class * 2) };
|
||||
},
|
||||
};
|
||||
if (parent_anchor_off == 0) continue;
|
||||
const parent_anchor = if (lookup.kind == .mark_lig)
|
||||
parent_array + try self.u16At(parent_array + 2 + @as(usize, pi) * 2) + parent_anchor_off
|
||||
else
|
||||
parent_array + parent_anchor_off;
|
||||
if (anchor_off == 0) continue;
|
||||
const parent_anchor = anchors_base + anchor_off;
|
||||
|
||||
return .{
|
||||
.dx = try self.i16At(parent_anchor + 2) - try self.i16At(mark_anchor + 2),
|
||||
.dy = try self.i16At(parent_anchor + 4) - try self.i16At(mark_anchor + 4),
|
||||
.dx = try self.intAt(i16, parent_anchor + 2) - try self.intAt(i16, mark_anchor + 2),
|
||||
.dy = try self.intAt(i16, parent_anchor + 4) - try self.intAt(i16, mark_anchor + 4),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -190,20 +216,25 @@ fn coverage(self: *const Gpos, off: usize, gid: u16) Error!?u16 {
|
||||
return null;
|
||||
},
|
||||
2 => {
|
||||
const count = try self.u16At(off + 2);
|
||||
for (0..count) |i| {
|
||||
const record = off + 4 + i * 6;
|
||||
const start = try self.u16At(record);
|
||||
const end = try self.u16At(record + 2);
|
||||
if (gid < start) return null;
|
||||
if (gid <= end) return try self.u16At(record + 4) + (gid - start);
|
||||
}
|
||||
return null;
|
||||
const record = try self.rangeRecord(off, gid) orelse return null;
|
||||
return try self.u16At(record + 4) + (gid - try self.u16At(record));
|
||||
},
|
||||
else => return error.MalformedFont,
|
||||
}
|
||||
}
|
||||
|
||||
// Coverage and ClassDef format 2 share the sorted (start, end, value)
|
||||
// record layout; returns the record holding `gid`.
|
||||
fn rangeRecord(self: *const Gpos, off: usize, gid: u16) Error!?usize {
|
||||
const count = try self.u16At(off + 2);
|
||||
for (0..count) |i| {
|
||||
const record = off + 4 + i * 6;
|
||||
if (gid < try self.u16At(record)) return null;
|
||||
if (gid <= try self.u16At(record + 2)) return record;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn classOf(self: *const Gpos, off: usize, gid: u16) Error!u16 {
|
||||
switch (try self.u16At(off)) {
|
||||
1 => {
|
||||
@@ -213,15 +244,8 @@ fn classOf(self: *const Gpos, off: usize, gid: u16) Error!u16 {
|
||||
return try self.u16At(off + 6 + (gid - start) * 2);
|
||||
},
|
||||
2 => {
|
||||
const count = try self.u16At(off + 2);
|
||||
for (0..count) |i| {
|
||||
const record = off + 4 + i * 6;
|
||||
const start = try self.u16At(record);
|
||||
const end = try self.u16At(record + 2);
|
||||
if (gid < start) return 0;
|
||||
if (gid <= end) return try self.u16At(record + 4);
|
||||
}
|
||||
return 0;
|
||||
const record = try self.rangeRecord(off, gid) orelse return 0;
|
||||
return try self.u16At(record + 4);
|
||||
},
|
||||
else => return error.MalformedFont,
|
||||
}
|
||||
@@ -245,16 +269,17 @@ fn bytesAt(self: *const Gpos, off: usize, len: usize) Error![]const u8 {
|
||||
return self.data[off..][0..len];
|
||||
}
|
||||
|
||||
fn u16At(self: *const Gpos, off: usize) Error!u16 {
|
||||
return std.mem.readInt(u16, (try self.bytesAt(off, 2))[0..2], .big);
|
||||
fn intAt(self: *const Gpos, comptime T: type, off: usize) Error!T {
|
||||
const n = @sizeOf(T);
|
||||
return std.mem.readInt(T, (try self.bytesAt(off, n))[0..n], .big);
|
||||
}
|
||||
|
||||
fn i16At(self: *const Gpos, off: usize) Error!i16 {
|
||||
return std.mem.readInt(i16, (try self.bytesAt(off, 2))[0..2], .big);
|
||||
fn u16At(self: *const Gpos, off: usize) Error!u16 {
|
||||
return self.intAt(u16, off);
|
||||
}
|
||||
|
||||
fn u32At(self: *const Gpos, off: usize) Error!u32 {
|
||||
return std.mem.readInt(u32, (try self.bytesAt(off, 4))[0..4], .big);
|
||||
return self.intAt(u32, off);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
@@ -304,10 +329,23 @@ test "browser.screenshot.gpos: DejaVu Sans mark attachment" {
|
||||
try testing.expectEqual(null, g.attachToMark(68, 690));
|
||||
}
|
||||
|
||||
test "browser.screenshot.gpos: DejaVu Sans metrics" {
|
||||
const g = try Gpos.init(testing.arena_allocator, @embedFile("fonts/DejaVuSans.ttf"));
|
||||
const m = try g.readMetrics();
|
||||
try testing.expectEqual(2048, m.units_per_em);
|
||||
try testing.expectEqual(1901, m.ascent);
|
||||
try testing.expectEqual(-483, m.descent);
|
||||
try testing.expectEqual(-40, m.underline_position);
|
||||
try testing.expectEqual(90, m.underline_thickness);
|
||||
try testing.expectEqual(530, m.strikeout_position);
|
||||
try testing.expectEqual(102, m.strikeout_size);
|
||||
}
|
||||
|
||||
test "browser.screenshot.gpos: all bundled faces parse" {
|
||||
inline for (.{ "DejaVuSans-Bold.ttf", "DejaVuSansMono.ttf", "DejaVuSansMono-Bold.ttf" }) |name| {
|
||||
const g = try Gpos.init(testing.arena_allocator, @embedFile("fonts/" ++ name));
|
||||
try testing.expectEqual(true, g.lookups.len > 0);
|
||||
try testing.expectEqual(true, g.glyph_class_def != null);
|
||||
try testing.expectEqual(2048, (try g.readMetrics()).units_per_em);
|
||||
}
|
||||
}
|
||||
+397
-481
File diff suppressed because it is too large.
Load diff
+7
-7
@@ -340,16 +340,16 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet
|
||||
}
|
||||
|
||||
const frame = page.frame();
|
||||
if (opts.dump_mode == .png and frame != null) {
|
||||
if (opts.dump_mode == .png) if (frame) |f| {
|
||||
const arena = try app.arena_pool.acquire(.large, "screenshot.dump");
|
||||
defer arena.release();
|
||||
|
||||
const shot = try screenshot.prepare(arena.allocator(), frame.?.window._document.asNode(), .{
|
||||
.width = frame.?._page.getViewport().width,
|
||||
}, frame.?);
|
||||
const shot = try screenshot.prepare(arena.allocator(), f.window._document.asNode(), .{
|
||||
.width = f._page.getViewport().width,
|
||||
}, f);
|
||||
try writeJsonEnvelope(writer, frame, opts.dump_mode, shot);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(app.allocator);
|
||||
defer aw.deinit();
|
||||
@@ -382,8 +382,8 @@ fn dumpContent(app: *App, mode: Config.DumpFormat, dump_opts: dump.Opts, frame:
|
||||
.html => try dump.root(frame.window._document, dump_opts, writer, frame),
|
||||
.markdown => try markdown.dump(frame.window._document.asNode(), .{}, writer, frame),
|
||||
.png => {
|
||||
var arena: std.heap.ArenaAllocator = .init(app.allocator);
|
||||
defer arena.deinit();
|
||||
const arena = try app.arena_pool.acquire(.large, "screenshot.dump");
|
||||
defer arena.release();
|
||||
_ = try screenshot.png(arena.allocator(), frame.window._document.asNode(), .{
|
||||
.width = frame._page.getViewport().width,
|
||||
}, writer, frame);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The ICU C API, from the copy bundled in V8's archive: bidi, Arabic
|
||||
//! shaping and line breaking. Symbols carry ICU's major version, so this
|
||||
//! moves with V8. The shared dev build of V8 only exports them from
|
||||
//! lightpanda-io/zig-v8-fork#201 on; before that, link the static archive.
|
||||
|
||||
const version = "77";
|
||||
|
||||
fn sym(comptime name: []const u8, comptime T: type) *const T {
|
||||
return @extern(*const T, .{ .name = name ++ "_" ++ version });
|
||||
}
|
||||
|
||||
pub const UBiDi = opaque {};
|
||||
pub const UBreakIterator = opaque {};
|
||||
|
||||
pub const ubidi_open = sym("ubidi_open", fn () callconv(.c) ?*UBiDi);
|
||||
pub const ubidi_close = sym("ubidi_close", fn (*UBiDi) callconv(.c) void);
|
||||
pub const ubidi_setPara = sym("ubidi_setPara", fn (*UBiDi, [*]const u16, i32, u8, ?[*]u8, *c_int) callconv(.c) void);
|
||||
pub const ubidi_getParaLevel = sym("ubidi_getParaLevel", fn (*const UBiDi) callconv(.c) u8);
|
||||
pub const ubidi_getLevels = sym("ubidi_getLevels", fn (*UBiDi, *c_int) callconv(.c) ?[*]const u8);
|
||||
pub const ubidi_reorderVisual = sym("ubidi_reorderVisual", fn ([*]const u8, i32, [*]i32) callconv(.c) void);
|
||||
pub const u_shapeArabic = sym("u_shapeArabic", fn ([*]const u16, i32, [*]u16, i32, u32, *c_int) callconv(.c) i32);
|
||||
pub const u_charMirror = sym("u_charMirror", fn (i32) callconv(.c) i32);
|
||||
pub const ubrk_open = sym("ubrk_open", fn (c_int, ?[*:0]const u8, ?[*]const u16, i32, *c_int) callconv(.c) ?*UBreakIterator);
|
||||
pub const ubrk_close = sym("ubrk_close", fn (*UBreakIterator) callconv(.c) void);
|
||||
pub const ubrk_next = sym("ubrk_next", fn (*UBreakIterator) callconv(.c) i32);
|
||||
pub const ubrk_getRuleStatus = sym("ubrk_getRuleStatus", fn (*UBreakIterator) callconv(.c) i32);
|
||||
|
||||
pub const UBIDI_DEFAULT_LTR: u8 = 0xfe;
|
||||
pub const UBRK_LINE: c_int = 2;
|
||||
pub const UBRK_DONE: i32 = -1;
|
||||
pub const UBRK_LINE_HARD: i32 = 100;
|
||||
pub const U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED: u32 = 0x18;
|
||||
|
||||
/// UErrorCode: warnings are negative, U_ZERO_ERROR is 0, failures positive.
|
||||
pub fn failed(err: c_int) bool {
|
||||
return err > 0;
|
||||
}
|
||||
Reference in new issue
Block a user