fix(build): preserve non-render compatibility

This commit is contained in:
SGavrl
2026-08-07 20:15:28 +00:00
parent af93379ba0
commit cd661982df
13 changed files with 239 additions and 49 deletions
+4 -4
View File
@@ -75,7 +75,7 @@ jobs:
"LIBCLANG_PATH=C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Build no-render release binary
run: cargo build --release --target ${{ matrix.target }} --no-default-features
run: cargo build --release --target ${{ matrix.target }} -p obscura-cli --bins --no-default-features
- name: Stage no-render release binary
shell: bash
@@ -89,7 +89,7 @@ jobs:
fi
- name: Build no-render stealth release binary
run: cargo build --release --target ${{ matrix.target }} --no-default-features --features stealth
run: cargo build --release --target ${{ matrix.target }} -p obscura-cli --bins --no-default-features --features stealth
- name: Stage no-render stealth release binary
shell: bash
@@ -103,7 +103,7 @@ jobs:
fi
- name: Build render-enabled release binary
run: cargo build --release --target ${{ matrix.target }} --features render
run: cargo build --release --target ${{ matrix.target }} -p obscura-cli --bins --features render
- name: Stage default release binary
shell: bash
@@ -117,7 +117,7 @@ jobs:
fi
- name: Build render-enabled stealth release binary
run: cargo build --release --target ${{ matrix.target }} --features render,stealth
run: cargo build --release --target ${{ matrix.target }} -p obscura-cli --bins --features render,stealth
- name: Stage stealth release binary
shell: bash
+4 -4
View File
@@ -13,14 +13,14 @@ capabilities. It targets web scraping and AI-agent automation.
## Build
```bash
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --features render
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --features render
# Rendering and stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --features render,stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --features render,stealth
# No rendering, with rustls or stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --no-default-features
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --no-default-features --features stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --no-default-features
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --no-default-features --features stealth
```
- The first build compiles V8 from source: ~5 minutes and a few GB of disk.
+4 -4
View File
@@ -201,16 +201,16 @@ git clone https://github.com/h4ckf0r0day/obscura.git
cd obscura
# Rendering
cargo build --release --features render
cargo build --release -p obscura-cli --bins --features render
# Rendering and stealth
cargo build --release --features render,stealth
cargo build --release -p obscura-cli --bins --features render,stealth
# No rendering
cargo build --release --no-default-features
cargo build --release -p obscura-cli --bins --no-default-features
# No rendering, with stealth
cargo build --release --no-default-features --features stealth
cargo build --release -p obscura-cli --bins --no-default-features --features stealth
```
Requires Rust 1.75+ ([rustup.rs](https://rustup.rs)). First build takes ~5 min (V8 compiles from source, cached after).
+29 -23
View File
@@ -4124,16 +4124,19 @@ mod tests {
"an inline import must fetch exactly once as a stylesheet"
);
}
for path in ["/local.svg", "/imported.svg"] {
assert_eq!(
observed_requests
.iter()
.filter(|(request_path, _)| request_path == path)
.map(|(_, resource_type)| *resource_type)
.collect::<Vec<_>>(),
vec![obscura_net::ResourceType::Image],
"ordinary rule assets must remain in render warmup"
);
#[cfg(feature = "render")]
{
for path in ["/local.svg", "/imported.svg"] {
assert_eq!(
observed_requests
.iter()
.filter(|(request_path, _)| request_path == path)
.map(|(_, resource_type)| *resource_type)
.collect::<Vec<_>>(),
vec![obscura_net::ResourceType::Image],
"ordinary rule assets must remain in render warmup"
);
}
}
drop(observed_requests);
@@ -4165,19 +4168,22 @@ mod tests {
assert!(styles[0].2.starts_with("@media print {\n"));
assert!(styles[1].2.starts_with("@media print {\n"));
let pdf = page
.raster_pdf(crate::RasterPdfOptions {
print_background: true,
paper_width_in: 100.0 / 72.0,
paper_height_in: 80.0 / 72.0,
margin_top_in: 0.0,
margin_bottom_in: 0.0,
margin_left_in: 0.0,
margin_right_in: 0.0,
..crate::RasterPdfOptions::default()
})
.expect("inline-import print PDF");
assert!(pdf.starts_with(b"%PDF-1.4"));
#[cfg(feature = "render")]
{
let pdf = page
.raster_pdf(crate::RasterPdfOptions {
print_background: true,
paper_width_in: 100.0 / 72.0,
paper_height_in: 80.0 / 72.0,
margin_top_in: 0.0,
margin_bottom_in: 0.0,
margin_left_in: 0.0,
margin_right_in: 0.0,
..crate::RasterPdfOptions::default()
})
.expect("inline-import print PDF");
assert!(pdf.starts_with(b"%PDF-1.4"));
}
}
fn client_replacement_page(name: &str, deferred: bool) -> super::Page {
+12 -2
View File
@@ -5540,7 +5540,11 @@ class HTMLImageElement extends Element {
}
_imageSourceChanged() {
this._adoptImageCandidate("");
// The lightweight build has no retained render-resource cache. It still
// preserves the historical non-blocking Image lifecycle so preloaders do
// not hang while rendering is disabled.
const hasMetadataLoader = typeof Deno.core.ops.op_load_image_metadata === "function";
this._adoptImageCandidate(hasMetadataLoader ? "" : this.src);
this._imageCompletionDeferred = true;
this._refreshImageFromCache(true);
if (!this._imageComplete) this._queueImageRequest();
@@ -7761,7 +7765,13 @@ globalThis.getComputedStyle = (el) => {
// Non-render builds and properties outside the renderer snapshot retain
// the lightweight inline CSSOM behavior.
const inlineVal = target.getPropertyValue ? target.getPropertyValue(rawProp) : '';
if (inlineVal) return inlineVal;
if (inlineVal) {
if (kebab === 'opacity') {
const value = Number(inlineVal);
if (Number.isFinite(value)) return String(Math.min(1, Math.max(0, value)));
}
return inlineVal;
}
const dim = dimensionFor(kebab);
if (dim != null) return dim;
if (defaultsKebab[rawProp]) return defaultsKebab[rawProp];
+1
View File
@@ -5475,6 +5475,7 @@ mod tests {
);
}
#[cfg(feature = "render")]
#[tokio::test(flavor = "current_thread")]
async fn raf_geometry_mutation_reaches_settled_intersection_before_next_frame() {
let mut rt = setup_runtime(
+6
View File
@@ -18465,6 +18465,7 @@ mod tests {
assert_eq!(laid.rects.get(&b).unwrap().y, 50.0);
}
#[cfg(feature = "paint")]
#[test]
fn text_transform_applies_to_word_leaves_in_flex_ui() {
let tree = parse_html(
@@ -18481,6 +18482,7 @@ mod tests {
);
}
#[cfg(feature = "paint")]
#[test]
fn word_leaves_use_the_computed_line_height() {
let tree = parse_html(
@@ -18989,6 +18991,7 @@ mod tests {
);
}
#[cfg(feature = "paint")]
#[test]
fn forced_break_in_pseudo_joined_inline_run_has_no_phantom_line() {
let tree = parse_html(
@@ -19184,6 +19187,7 @@ mod tests {
);
}
#[cfg(feature = "paint")]
#[test]
fn ordinary_inline_fragment_uses_font_box_while_line_keeps_line_height() {
let tree = parse_html(
@@ -19233,6 +19237,7 @@ mod tests {
);
}
#[cfg(feature = "paint")]
#[test]
fn nested_inline_uses_final_shaping_for_its_canonical_fragment() {
let tree = parse_html(
@@ -19364,6 +19369,7 @@ mod tests {
assert!(fragments.iter().all(|fragment| fragment.width <= 70.01));
}
#[cfg(feature = "paint")]
#[test]
fn empty_decorated_inline_has_a_box_and_advances_following_text() {
let tree = parse_html(
+165
View File
@@ -141,6 +141,14 @@ pub mod inline {
use obscura_dom::tree::{DomTree, NodeId};
use std::collections::HashMap;
#[derive(Clone)]
pub(crate) struct WebFont {
pub data: Vec<u8>,
pub family: Option<String>,
pub weight: Option<(u16, u16)>,
pub italic: Option<bool>,
}
#[derive(Default)]
pub struct TextEngine;
@@ -148,6 +156,27 @@ pub mod inline {
pub fn new() -> Self {
TextEngine
}
pub(crate) fn new_with_web_fonts(_fonts: &[WebFont]) -> Self {
TextEngine
}
pub fn register_replaced(
&mut self,
_width: f32,
_height: f32,
_style: &crate::LayoutStyle,
) -> usize {
0
}
pub(crate) fn register_replaced_intrinsic(
&mut self,
_intrinsic: crate::ReplacedIntrinsic,
_style: &crate::LayoutStyle,
) -> usize {
0
}
/// Layout-only builds have no shaper, so no container is ever treated
/// as a cosmic-text inline formatting context: the word-split path
/// handles text geometry for `getBoundingClientRect`.
@@ -202,6 +231,142 @@ pub mod inline {
(0.0, 0.0)
}
}
pub(crate) fn used_line_height(style: &crate::LayoutStyle) -> f32 {
TextEngine.selected_line_height(style)
}
pub(crate) fn is_replaced(local: &str) -> bool {
matches!(
local,
"img"
| "svg"
| "canvas"
| "video"
| "audio"
| "iframe"
| "embed"
| "object"
| "input"
| "textarea"
| "select"
| "button"
| "progress"
| "meter"
)
}
pub(crate) fn has_replaced_sizing(local: &str) -> bool {
matches!(
local,
"img"
| "canvas"
| "video"
| "audio"
| "iframe"
| "embed"
| "object"
| "progress"
| "meter"
)
}
pub(crate) fn default_replaced_intrinsic_size(
local: &str,
font_size: f32,
has_controls: bool,
has_resource: bool,
) -> Option<(f32, f32)> {
match local {
"canvas" | "video" | "iframe" | "object" => Some((300.0, 150.0)),
"embed" if has_resource => Some((300.0, 150.0)),
"audio" if has_controls => Some((300.0, 54.0)),
"progress" => Some((font_size * 10.0, font_size)),
"meter" => Some((font_size * 5.0, font_size)),
_ => None,
}
}
pub(crate) fn constrained_auto_replaced_size(
width: f32,
height: f32,
style: &crate::LayoutStyle,
) -> taffy::Size<f32> {
let px = |dimension| match dimension {
crate::Dimension::Px(value) => Some(value.max(0.0)),
_ => None,
};
let ratio = style
.aspect_ratio
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
.unwrap_or_else(|| {
if width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0 {
width / height
} else {
2.0
}
});
let preferred_width = px(style.width);
let preferred_height = px(style.height);
let min_width = px(style.min_width).unwrap_or(0.0);
let min_height = px(style.min_height).unwrap_or(0.0);
let max_width = px(style.max_width)
.unwrap_or(f32::INFINITY)
.max(min_width);
let max_height = px(style.max_height)
.unwrap_or(f32::INFINITY)
.max(min_height);
let (width, height) = match (preferred_width, preferred_height) {
(Some(width), Some(height)) => (
width.min(max_width).max(min_width),
height.min(max_height).max(min_height),
),
(Some(width), None) => {
let width = width.min(max_width).max(min_width);
(width, (width / ratio).min(max_height).max(min_height))
}
(None, Some(height)) => {
let height = height.min(max_height).max(min_height);
((height * ratio).min(max_width).max(min_width), height)
}
(None, None) => {
let height_at_max_width = (max_width / ratio).max(min_height);
let height_at_min_width = (min_width / ratio).min(max_height);
let width_at_max_height = (max_height * ratio).max(min_width);
let width_at_min_height = (min_height * ratio).min(max_width);
if width > max_width {
if height > max_height {
if max_width * height <= max_height * width {
(max_width, height_at_max_width)
} else {
(width_at_max_height, max_height)
}
} else {
(max_width, height_at_max_width)
}
} else if width < min_width {
if height < min_height {
if min_width * height <= min_height * width {
(width_at_min_height, min_height)
} else {
(min_width, height_at_min_width)
}
} else {
(min_width, height_at_min_width)
}
} else if height > max_height {
(width_at_max_height, max_height)
} else if height < min_height {
(width_at_min_height, min_height)
} else {
(width, height)
}
}
};
taffy::Size { width, height }
}
}
/// An axis-aligned rectangle in CSS pixels, relative to the containing block.
+1
View File
@@ -10,6 +10,7 @@ license = "Apache-2.0"
[features]
default = ["api"]
api = ["obscura-browser", "obscura-net", "tokio"]
render = ["api", "obscura-browser/render"]
[dependencies]
obscura-browser = { path = "../obscura-browser", optional = true }
+1
View File
@@ -113,6 +113,7 @@ async fn new_image_still_emulates_load_when_src_is_configurable() {
);
}
#[cfg(feature = "render")]
#[tokio::test]
async fn invalid_image_bytes_emit_error_like_chromium() {
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
+6 -6
View File
@@ -11,7 +11,7 @@ First build takes about 5 minutes. Incremental builds are seconds.
```bash
git clone https://github.com/h4ckf0r0day/obscura.git
cd obscura
cargo build --release --features render
cargo build --release -p obscura-cli --bins --features render
```
Binary is at `./target/release/obscura`.
@@ -22,7 +22,7 @@ and PDF export.
## Rendering and stealth
```bash
cargo build --release --features render,stealth
cargo build --release -p obscura-cli --bins --features render,stealth
```
This is the complete rendering build with the stealth wreq/BoringSSL transport,
@@ -32,8 +32,8 @@ blocklist. See [Configure stealth and proxies](Configure-stealth-and-proxies.md)
## Without rendering
```bash
cargo build --release --no-default-features
cargo build --release --no-default-features --features stealth
cargo build --release -p obscura-cli --bins --no-default-features
cargo build --release -p obscura-cli --bins --no-default-features --features stealth
```
The second command keeps stealth while excluding layout, screenshots,
@@ -58,7 +58,7 @@ libc++ while compiling BoringSSL. Use the active SDK for that build:
```bash
SDK_PATH="$(xcrun --show-sdk-path)"
SDKROOT="$SDK_PATH" CXXFLAGS="-isystem $SDK_PATH/usr/include/c++/v1" \
cargo build --release --features render,stealth
cargo build --release -p obscura-cli --bins --features render,stealth
```
## OpenSSL on older systems
@@ -66,7 +66,7 @@ SDKROOT="$SDK_PATH" CXXFLAGS="-isystem $SDK_PATH/usr/include/c++/v1" \
If the build fails on the vendored OpenSSL with an AVX-512 assembler error (common on older VPS hosts):
```bash
OPENSSL_NO_VENDOR=1 cargo build --release --features render
OPENSSL_NO_VENDOR=1 cargo build --release -p obscura-cli --bins --features render
```
Uses the system OpenSSL instead.
+2 -2
View File
@@ -20,10 +20,10 @@ with rendering or a `-no-render-stealth` archive without it. To build the
rendering variant yourself:
```bash
cargo build --release --features render,stealth
cargo build --release -p obscura-cli --bins --features render,stealth
```
Omit rendering with `cargo build --release --no-default-features --features stealth`.
Omit rendering with `cargo build --release -p obscura-cli --bins --no-default-features --features stealth`.
## What stealth handles
+4 -4
View File
@@ -16,21 +16,21 @@ Official release archives and Docker images include rendering. For a source
checkout, build release mode with the render feature:
```bash
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --features render
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --features render
```
Build rendering and stealth together for the wreq/BoringSSL transport,
browser-identity protections, and tracker blocking:
```bash
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --features render,stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --features render,stealth
```
Build without rendering when only DOM, extraction, or CDP automation is needed:
```bash
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --no-default-features
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release --no-default-features --features stealth
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --no-default-features
CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo build --release -p obscura-cli --bins --no-default-features --features stealth
```
Use `./target/release/obscura` in the commands below when working from source.