Skip to content
Closed
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ reasoned allow only when the lint is a genuine false positive.
single `browser_oxide` crate (`crates/browser_oxide`), organized into
modules — `dom`, `html_parser`, `js_runtime`, `event_loop`, `net`,
`stealth`, `canvas`, `workers`, `css_parser`/`css_selectors`/
`css_values`/`css_cascade`, `layout`, `protocol`, `host` — each with a
single responsibility (see `docs/ARCHITECTURE.md`). The workspace
`css_values`/`css_cascade`, `style`, `layout`, `render`, `protocol`, `host` —
each with a single responsibility (see `docs/ARCHITECTURE.md`). The workspace
publishes exactly two crates: `browser_oxide` and the `browser_oxide_mcp`
server. `browser_oxide_py` is a standalone (PyPI) workspace.
- **Per-vendor challenge solving is out of scope here.** The engine
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/browser_oxide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
] }
fontdb = { version = "0.23", default-features = false }
rustybuzz = "0.20"
# Inline layout. All MIT OR Apache-2.0, so deny.toml needs no exception.
unicode-bidi = "0.3"

Check failure on line 92 in crates/browser_oxide/Cargo.toml

View workflow job for this annotation

GitHub Actions / Unused deps (cargo-shear)

shear/unused_dependency

unused dependency `unicode-bidi` (remove this dependency)
unicode-linebreak = "0.1"
unicode-segmentation = "1"

Check failure on line 94 in crates/browser_oxide/Cargo.toml

View workflow job for this annotation

GitHub Actions / Unused deps (cargo-shear)

shear/unused_dependency

unused dependency `unicode-segmentation` (remove this dependency)
unicode-script = "0.5"
ttf-parser = "0.25"
swash = "0.2"
rustfft = "6"
glow = { version = "0.18", optional = true }
Expand Down
47 changes: 47 additions & 0 deletions crates/browser_oxide/examples/screenshot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Render a page to a PNG.
//!
//! ```text
//! cargo run --release -p browser_oxide --example screenshot -- https://example.com out.png
//! cargo run --release -p browser_oxide --example screenshot -- page.html out.png
//! ```
//!
//! The engine could not do this at all until the `render` module existed: Skia
//! was a dependency used only by `<canvas>`, and the CDP surface implemented 48
//! methods of which `Page.captureScreenshot` was not one.

use browser_oxide::Page;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let target = args.next().unwrap_or_else(|| {
eprintln!("usage: screenshot <url|file.html> [out.png] [width] [height]");
std::process::exit(2);
});
let out = args.next().unwrap_or_else(|| "screenshot.png".to_string());
let width: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(1280);
let height: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(800);

let mut page = if target.starts_with("http://") || target.starts_with("https://") {
let profile = browser_oxide::stealth::presets::chrome_148_linux();
let client = browser_oxide::net::HttpClient::new(&profile)?;
Page::navigate_simple(&target, &client, profile).await?
} else {
let html = std::fs::read_to_string(&target)?;
Page::from_html(&html, None).await?
};

let started = std::time::Instant::now();
let png = page
.screenshot_png(width, height)
.ok_or("nothing to capture: the page has no document")?;
let elapsed = started.elapsed();

std::fs::write(&out, &png)?;
println!(
"wrote {out} — {width}x{height}, {} KiB, {:.1} ms",
png.len() / 1024,
elapsed.as_secs_f64() * 1000.0
);
Ok(())
}
8 changes: 8 additions & 0 deletions crates/browser_oxide/src/css_cascade/computed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ fn all_property_ids() -> Vec<PropertyId> {
PropertyId::BorderRightWidth,
PropertyId::BorderBottomWidth,
PropertyId::BorderLeftWidth,
PropertyId::BorderTopStyle,
PropertyId::BorderRightStyle,
PropertyId::BorderBottomStyle,
PropertyId::BorderLeftStyle,
PropertyId::BorderTopColor,
PropertyId::BorderRightColor,
PropertyId::BorderBottomColor,
PropertyId::BorderLeftColor,
PropertyId::BoxSizing,
PropertyId::OverflowX,
PropertyId::OverflowY,
Expand Down
19 changes: 19 additions & 0 deletions crates/browser_oxide/src/css_cascade/initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ pub fn initial_value(property: &PropertyId) -> CssValue {
| PropertyId::PaddingLeft => {
CssValue::LengthPercentage(LengthPercentage::Length(Length::Zero))
}
PropertyId::BorderTopStyle
| PropertyId::BorderRightStyle
| PropertyId::BorderBottomStyle
| PropertyId::BorderLeftStyle => {
CssValue::BorderStyle(crate::css_values::types::display::BorderStyle::None)
}
// `currentColor`. The engine has no keyword for it, so the initial is
// the initial `color` — correct for the overwhelmingly common case
// where `color` is not set on the element itself, and closer than
// transparent for the rest.
PropertyId::BorderTopColor
| PropertyId::BorderRightColor
| PropertyId::BorderBottomColor
| PropertyId::BorderLeftColor => CssValue::Color(Color::Rgba {
r: 0,
g: 0,
b: 0,
a: 1.0,
}),
PropertyId::BorderTopWidth
| PropertyId::BorderRightWidth
| PropertyId::BorderBottomWidth
Expand Down
Loading
Loading