Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8880aa2f35 | ||
|
|
f051e72f85 | ||
|
|
6ae703565c | ||
|
|
d56442cf91 | ||
|
|
dad7be8c77 | ||
|
|
7921928ec4 | ||
|
|
a71198d591 | ||
|
|
19aefb7eb8 | ||
|
|
ff2e66c0a1 | ||
|
|
d320e1df47 | ||
|
|
7b454e14ed | ||
|
|
17d44baa2f | ||
|
|
59589043db | ||
|
|
9e7c8937a6 | ||
|
|
2d0d18f0d9 | ||
|
|
ae22bda46c |
@@ -235,15 +235,23 @@ jobs:
|
||||
id: publish_npm
|
||||
if: steps.changesets.outputs.hasChangesets == 'false' && steps.publish_check.outputs.needs_publish == 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ""
|
||||
NODE_AUTH_TOKEN: ''
|
||||
NPM_CONFIG_USERCONFIG: /home/runner/work/_temp/trusted-npmrc
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
NPM_CONFIG_PROVENANCE: 'true'
|
||||
run: |
|
||||
npm install -g npm@^11
|
||||
npm --version
|
||||
printf "registry=https://registry.npmjs.org/\n" > "$NPM_CONFIG_USERCONFIG"
|
||||
pnpm ci:publish
|
||||
|
||||
- name: Verify published npm tarball
|
||||
if: steps.publish_npm.outcome == 'success'
|
||||
env:
|
||||
PACKAGE_NAME: agent-browser-stealth
|
||||
EXPECTED_VERSION: ${{ steps.publish_check.outputs.local_version }}
|
||||
run: |
|
||||
pnpm run verify:registry-host-binary
|
||||
|
||||
- name: Set release outputs
|
||||
id: publish_metadata
|
||||
run: |
|
||||
@@ -294,7 +302,7 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
TAG="v$VERSION"
|
||||
|
||||
|
||||
# Check if release already exists
|
||||
if gh release view "$TAG" &>/dev/null; then
|
||||
echo "Release $TAG already exists, uploading binaries..."
|
||||
|
||||
+3
-3
@@ -4,10 +4,10 @@ node_modules/
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Native binaries (keep the launcher scripts)
|
||||
# Native binaries and build byproducts (keep JS launcher)
|
||||
bin/agent-browser-*
|
||||
!bin/agent-browser
|
||||
!bin/agent-browser.cmd
|
||||
bin/agent-browser
|
||||
bin/*.d
|
||||
|
||||
# Rust build artifacts
|
||||
cli/target/
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# agent-browser
|
||||
|
||||
## 0.16.3-fork.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Sync upstream `v0.16.2` / `v0.16.3` core fixes into the fork baseline.
|
||||
- Import headed-mode behavior updates from upstream.
|
||||
- Improve CDP debug-port discovery by switching to `reqwest` in native Chrome probing.
|
||||
- Fix dialog dismiss command parsing consistency.
|
||||
- Surface daemon startup stderr on launch failure to avoid opaque timeout-only errors.
|
||||
- Keep fork stealth hardening for anti-debug self-destruct flows (`disable-devtool-auto` bootstrap neutralization).
|
||||
|
||||
## 0.16.1-fork.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Harden runtime stealth against anti-debug self-destruct flows on high-risk sites:
|
||||
- neutralize `disable-devtool` auto bootstrap probes by hiding the `[disable-devtool-auto]` selector entry point
|
||||
- preserve normal selector behavior for non-target queries to minimize side effects
|
||||
- add regression tests covering the selector patch boundary
|
||||
- Expand security design docs with the anti-debug execution-plane model and clarify why page self-close/redirect is a separate surface from fingerprint scoring.
|
||||
|
||||
## 0.15.2-fork.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -81,7 +81,10 @@ Install once in Chrome: load unpacked extension from `extensions/tab-group-cdp/`
|
||||
- Download archive routing: downloads from managed tabs are routed to `agent-browser-stealth/<session>/...`.
|
||||
- Domain allowlist fallback: when allowlist is configured for a session, extension can force-block out-of-policy tabs to `about:blank`.
|
||||
- Risk hints (debug only): suspicious host/TLD hints are returned via handshake and printed only when `AGENT_BROWSER_DEBUG=1`.
|
||||
- Side panel console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, edit session allowlist, and toggle auto-clean.
|
||||
- Side panel browser controls: open/back/forward/reload, click/fill/press by CSS selector, run shortcut commands, and switch/close tabs.
|
||||
- Side panel developer signals: capture page console errors/warnings, fetch/xhr network events, command history, and live DOM snapshots.
|
||||
- Workflow automation: record actions into workflows, run workflows, map workflows to slash shortcuts, and schedule runs (daily/weekly/monthly/yearly).
|
||||
- Side panel operations console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, edit session allowlist, and toggle auto-clean.
|
||||
|
||||
## Stealth Architecture
|
||||
|
||||
@@ -229,7 +232,7 @@ flowchart TD
|
||||
## Operational Recommendations
|
||||
|
||||
- Prefer `--headed` for high-friction targets.
|
||||
- Reuse session state with one stable `--session-name` for continuity (when omitted, it defaults to `--session`).
|
||||
- Reuse session state with one stable `--session-name` for continuity (when omitted, it defaults to `default`).
|
||||
- Keep locale/timezone consistent with target market.
|
||||
- For challenge-heavy pages, prefer `--wait-until domcontentloaded` on `open`/`navigate` to avoid `load` stalls.
|
||||
- Use `--risk-mode block` in strict pipelines that require explicit operator intervention on verification pages.
|
||||
|
||||
Generated
+1
-3
@@ -45,7 +45,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.16.1-fork.0"
|
||||
version = "0.16.3-fork.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
@@ -2821,8 +2821,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||||
dependencies = [
|
||||
"zune-core 0.5.1",
|
||||
]
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.16.1-fork.2"
|
||||
version = "0.16.3-fork.2"
|
||||
edition = "2021"
|
||||
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
|
||||
license = "Apache-2.0"
|
||||
@@ -51,3 +51,6 @@ strip = true
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
|
||||
[patch.crates-io]
|
||||
zune-jpeg = { path = "vendor/zune-jpeg" }
|
||||
|
||||
@@ -939,6 +939,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some("dismiss") => {
|
||||
let mut cmd = json!({ "id": id, "action": "dialog", "response": "dismiss" });
|
||||
if let Some(prompt_text) = rest.get(1) {
|
||||
cmd["promptText"] = json!(prompt_text);
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: VALID,
|
||||
|
||||
+46
-53
@@ -278,6 +278,10 @@ pub fn ensure_daemon(
|
||||
.find(|p| p.exists())
|
||||
.ok_or("Daemon not found. Set AGENT_BROWSER_HOME environment variable or run from project directory.")?;
|
||||
|
||||
// Keep handle to detect early daemon exit and surface startup errors.
|
||||
#[allow(unused_assignments)]
|
||||
let mut daemon_child: Option<std::process::Child> = None;
|
||||
|
||||
// Spawn daemon as a fully detached background process
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -363,11 +367,13 @@ pub fn ensure_daemon(
|
||||
});
|
||||
}
|
||||
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?;
|
||||
daemon_child = Some(
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -451,12 +457,14 @@ pub fn ensure_daemon(
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?;
|
||||
daemon_child = Some(
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {}", e))?,
|
||||
);
|
||||
}
|
||||
|
||||
for _ in 0..50 {
|
||||
@@ -465,6 +473,22 @@ pub fn ensure_daemon(
|
||||
already_running: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Surface daemon startup stderr instead of returning an opaque timeout.
|
||||
if let Some(ref mut child) = daemon_child {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
let mut stderr_output = String::new();
|
||||
if let Some(mut stderr) = child.stderr.take() {
|
||||
let _ = stderr.read_to_string(&mut stderr_output);
|
||||
}
|
||||
let stderr_trimmed = stderr_output.trim();
|
||||
if !stderr_trimmed.is_empty() {
|
||||
return Err(format!("Daemon failed to start: {}", stderr_trimmed));
|
||||
}
|
||||
return Err("Daemon failed to start: process exited during startup".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
@@ -569,45 +593,14 @@ fn send_command_once(cmd: &Value, session: &str) -> Result<Response, String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
// Mutex to prevent parallel tests from interfering with env vars
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// RAII guard that locks env mutex and restores env vars on drop
|
||||
struct EnvGuard<'a> {
|
||||
_lock: MutexGuard<'a, ()>,
|
||||
vars: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl<'a> EnvGuard<'a> {
|
||||
fn new(var_names: &[&str]) -> Self {
|
||||
let lock = ENV_MUTEX.lock().unwrap();
|
||||
let vars = var_names
|
||||
.iter()
|
||||
.map(|&name| (name.to_string(), env::var(name).ok()))
|
||||
.collect();
|
||||
Self { _lock: lock, vars }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
for (name, value) in &self.vars {
|
||||
match value {
|
||||
Some(v) => env::set_var(name, v),
|
||||
None => env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::test_utils::EnvGuard;
|
||||
|
||||
#[test]
|
||||
fn test_get_socket_dir_explicit_override() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "/custom/socket/path");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
_guard.set("AGENT_BROWSER_SOCKET_DIR", "/custom/socket/path");
|
||||
_guard.remove("XDG_RUNTIME_DIR");
|
||||
|
||||
assert_eq!(get_socket_dir(), PathBuf::from("/custom/socket/path"));
|
||||
}
|
||||
@@ -616,8 +609,8 @@ mod tests {
|
||||
fn test_get_socket_dir_ignores_empty_socket_dir() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
_guard.set("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
_guard.remove("XDG_RUNTIME_DIR");
|
||||
|
||||
assert!(get_socket_dir()
|
||||
.to_string_lossy()
|
||||
@@ -628,8 +621,8 @@ mod tests {
|
||||
fn test_get_socket_dir_xdg_runtime() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
|
||||
env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
|
||||
_guard.remove("AGENT_BROWSER_SOCKET_DIR");
|
||||
_guard.set("XDG_RUNTIME_DIR", "/run/user/1000");
|
||||
|
||||
assert_eq!(
|
||||
get_socket_dir(),
|
||||
@@ -641,8 +634,8 @@ mod tests {
|
||||
fn test_get_socket_dir_ignores_empty_xdg_runtime() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
env::set_var("XDG_RUNTIME_DIR", "");
|
||||
_guard.set("AGENT_BROWSER_SOCKET_DIR", "");
|
||||
_guard.set("XDG_RUNTIME_DIR", "");
|
||||
|
||||
assert!(get_socket_dir()
|
||||
.to_string_lossy()
|
||||
@@ -653,8 +646,8 @@ mod tests {
|
||||
fn test_get_socket_dir_home_fallback() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
|
||||
|
||||
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
|
||||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
_guard.remove("AGENT_BROWSER_SOCKET_DIR");
|
||||
_guard.remove("XDG_RUNTIME_DIR");
|
||||
|
||||
let result = get_socket_dir();
|
||||
assert!(result.to_string_lossy().ends_with(".agent-browser"));
|
||||
|
||||
+21
-25
@@ -1,4 +1,4 @@
|
||||
use crate::{color, validation};
|
||||
use crate::color;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
@@ -214,7 +214,7 @@ pub struct Flags {
|
||||
pub allow_file_access: bool,
|
||||
pub device: Option<String>,
|
||||
pub auto_connect: bool,
|
||||
pub session_name: Option<String>, // Defaults to --session when unset
|
||||
pub session_name: Option<String>, // Defaults to "default" when unset
|
||||
pub annotate: bool,
|
||||
pub color_scheme: Option<String>,
|
||||
pub download_path: Option<String>,
|
||||
@@ -273,10 +273,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
Err(_) => config.headed.unwrap_or(true),
|
||||
},
|
||||
debug: env_var_is_truthy("AGENT_BROWSER_DEBUG") || config.debug.unwrap_or(false),
|
||||
session: env::var("AGENT_BROWSER_SESSION")
|
||||
.ok()
|
||||
.or(config.session)
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
// --session is disabled: user-facing CLI always uses one default session.
|
||||
session: "default".to_string(),
|
||||
headers: config.headers,
|
||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
|
||||
.ok()
|
||||
@@ -368,12 +366,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--session" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.session = s.clone();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--headers" => {
|
||||
if let Some(h) = args.get(i + 1) {
|
||||
flags.headers = Some(h.clone());
|
||||
@@ -532,14 +524,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
}
|
||||
|
||||
// Keep auth/state continuity stable by default: if no explicit --session-name
|
||||
// is provided, derive it from --session (or fall back to "default" when invalid).
|
||||
// is provided, derive it from the default session id.
|
||||
if flags.session_name.is_none() {
|
||||
let derived = if validation::is_valid_session_name(&flags.session) {
|
||||
flags.session.clone()
|
||||
} else {
|
||||
"default".to_string()
|
||||
};
|
||||
flags.session_name = Some(derived);
|
||||
flags.session_name = Some("default".to_string());
|
||||
}
|
||||
|
||||
flags
|
||||
@@ -753,27 +740,36 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_flags_with_session_and_executable_path() {
|
||||
fn test_parse_flags_ignores_session_flag_and_keeps_default_session() {
|
||||
let flags = parse_flags(&args(
|
||||
"--session test --executable-path /custom/chrome open example.com",
|
||||
));
|
||||
assert_eq!(flags.session, "test");
|
||||
assert_eq!(flags.session, "default");
|
||||
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("test"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_name_defaults_to_session_when_not_provided() {
|
||||
fn test_session_name_defaults_to_default_when_not_provided() {
|
||||
let flags = parse_flags(&args("--session my-session snapshot"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("my-session"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_session_falls_back_to_default_session_name() {
|
||||
fn test_invalid_session_still_uses_default_session_name() {
|
||||
let flags = parse_flags(&args("--session bad/session snapshot"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_session_is_ignored_and_default_session_is_used() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_SESSION"]);
|
||||
env::set_var("AGENT_BROWSER_SESSION", "legacy-session");
|
||||
let flags = parse_flags(&args("snapshot"));
|
||||
assert_eq!(flags.session, "default");
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_executable_path_tracking() {
|
||||
// When --executable-path is passed via CLI, cli_executable_path should be true
|
||||
|
||||
@@ -4,6 +4,8 @@ mod connection;
|
||||
mod flags;
|
||||
mod install;
|
||||
mod output;
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
mod validation;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
@@ -755,6 +755,13 @@ fn launch_options_from_env() -> LaunchOptions {
|
||||
.map(|v| v == "1" || v == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let extensions: Option<Vec<String>> = env::var("AGENT_BROWSER_EXTENSIONS").ok().map(|v| {
|
||||
v.split([',', '\n'])
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
});
|
||||
|
||||
LaunchOptions {
|
||||
headless: !headed,
|
||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
||||
@@ -772,12 +779,7 @@ fn launch_options_from_env() -> LaunchOptions {
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
extensions: env::var("AGENT_BROWSER_EXTENSIONS").ok().map(|v| {
|
||||
v.split([',', '\n'])
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
}),
|
||||
extensions,
|
||||
storage_state: env::var("AGENT_BROWSER_STATE").ok(),
|
||||
user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok(),
|
||||
ignore_https_errors: env::var("AGENT_BROWSER_IGNORE_HTTPS_ERRORS")
|
||||
@@ -866,6 +868,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
});
|
||||
|
||||
let profile = cmd.get("profile").and_then(|v| v.as_str());
|
||||
let storage_state = cmd.get("storageState").and_then(|v| v.as_str());
|
||||
let allow_file_access = cmd
|
||||
@@ -2877,7 +2880,12 @@ async fn handle_permissions(cmd: &Value, state: &DaemonState) -> Result<Value, S
|
||||
|
||||
async fn handle_dialog(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let accept = cmd.get("accept").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
let accept = cmd
|
||||
.get("response")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|r| r == "accept")
|
||||
.or_else(|| cmd.get("accept").and_then(|v| v.as_bool()))
|
||||
.unwrap_or(true);
|
||||
let prompt_text = cmd.get("promptText").and_then(|v| v.as_str());
|
||||
|
||||
mgr.handle_dialog(accept, prompt_text).await?;
|
||||
@@ -5124,6 +5132,7 @@ fn error_response(id: &str, error: &str) -> Value {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::EnvGuard;
|
||||
|
||||
#[test]
|
||||
fn test_success_response_structure() {
|
||||
@@ -5160,6 +5169,14 @@ mod tests {
|
||||
assert!(!opts.allow_file_access);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_options_from_env_headed_flag() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_HEADED"]);
|
||||
_guard.set("AGENT_BROWSER_HEADED", "1");
|
||||
let opts = launch_options_from_env();
|
||||
assert!(!opts.headless, "AGENT_BROWSER_HEADED=1 should set headless=false");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_unknown_command() {
|
||||
let mut state = DaemonState::new();
|
||||
|
||||
+214
-66
@@ -8,6 +8,7 @@ use super::types::BrowserVersionInfo;
|
||||
pub struct ChromeProcess {
|
||||
child: Child,
|
||||
pub ws_url: String,
|
||||
temp_user_data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ChromeProcess {
|
||||
@@ -20,6 +21,23 @@ impl ChromeProcess {
|
||||
impl Drop for ChromeProcess {
|
||||
fn drop(&mut self) {
|
||||
self.kill();
|
||||
if let Some(ref dir) = self.temp_user_data_dir {
|
||||
for attempt in 0..3 {
|
||||
match std::fs::remove_dir_all(dir) {
|
||||
Ok(()) => break,
|
||||
Err(_) if attempt < 2 => {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: failed to clean up temp profile {}: {}",
|
||||
dir.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,14 +77,12 @@ impl Default for LaunchOptions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
let chrome_path = match &options.executable_path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => {
|
||||
find_chrome().ok_or("Chrome not found. Install Chrome or use --executable-path.")?
|
||||
}
|
||||
};
|
||||
struct ChromeArgs {
|
||||
args: Vec<String>,
|
||||
temp_user_data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
|
||||
let mut args = vec![
|
||||
"--remote-debugging-port=0".to_string(),
|
||||
"--no-first-run".to_string(),
|
||||
@@ -97,10 +113,18 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
args.push(format!("--proxy-bypass-list={}", bypass));
|
||||
}
|
||||
|
||||
if let Some(ref profile) = options.profile {
|
||||
let temp_user_data_dir = if let Some(ref profile) = options.profile {
|
||||
let expanded = expand_tilde(profile);
|
||||
args.push(format!("--user-data-dir={}", expanded));
|
||||
}
|
||||
None
|
||||
} else {
|
||||
let dir = std::env::temp_dir()
|
||||
.join(format!("agent-browser-chrome-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir)
|
||||
.map_err(|e| format!("Failed to create temp profile dir: {}", e))?;
|
||||
args.push(format!("--user-data-dir={}", dir.display()));
|
||||
Some(dir)
|
||||
};
|
||||
|
||||
if options.allow_file_access {
|
||||
args.push("--allow-file-access-from-files".to_string());
|
||||
@@ -115,7 +139,6 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user args set window size (skip viewport override)
|
||||
let has_window_size = options
|
||||
.args
|
||||
.iter()
|
||||
@@ -131,23 +154,66 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
args.push("--no-sandbox".to_string());
|
||||
}
|
||||
|
||||
Ok(ChromeArgs {
|
||||
args,
|
||||
temp_user_data_dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
let chrome_path = match &options.executable_path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => {
|
||||
find_chrome().ok_or("Chrome not found. Install Chrome or use --executable-path.")?
|
||||
}
|
||||
};
|
||||
|
||||
let ChromeArgs {
|
||||
args,
|
||||
temp_user_data_dir,
|
||||
} = build_chrome_args(options)?;
|
||||
|
||||
let cleanup_temp_dir = |dir: &Option<PathBuf>| {
|
||||
if let Some(ref d) = dir {
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
};
|
||||
|
||||
let mut child = Command::new(&chrome_path)
|
||||
.args(&args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to launch Chrome at {:?}: {}", chrome_path, e))?;
|
||||
.map_err(|e| {
|
||||
cleanup_temp_dir(&temp_user_data_dir);
|
||||
format!("Failed to launch Chrome at {:?}: {}", chrome_path, e)
|
||||
})?;
|
||||
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or("Failed to capture Chrome stderr")?;
|
||||
.ok_or_else(|| {
|
||||
let _ = child.kill();
|
||||
cleanup_temp_dir(&temp_user_data_dir);
|
||||
"Failed to capture Chrome stderr".to_string()
|
||||
})?;
|
||||
let reader = BufReader::new(stderr);
|
||||
|
||||
let ws_url = wait_for_ws_url(reader)?;
|
||||
let ws_url = match wait_for_ws_url(reader) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
let _ = child.kill();
|
||||
cleanup_temp_dir(&temp_user_data_dir);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ChromeProcess { child, ws_url })
|
||||
Ok(ChromeProcess {
|
||||
child,
|
||||
ws_url,
|
||||
temp_user_data_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn wait_for_ws_url(reader: BufReader<std::process::ChildStderr>) -> Result<String, String> {
|
||||
@@ -315,55 +381,8 @@ pub async fn discover_cdp_url(port: u16) -> Result<String, String> {
|
||||
}
|
||||
|
||||
async fn reqwest_get_string(url: &str) -> Result<String, String> {
|
||||
let client = tokio::net::TcpStream::connect(
|
||||
url.strip_prefix("http://")
|
||||
.unwrap_or(url)
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or("127.0.0.1:9222"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let path = url
|
||||
.find('/')
|
||||
.and_then(|i| url[i..].find('/').map(|j| &url[i + j..]))
|
||||
.unwrap_or("/json/version");
|
||||
|
||||
let host = url
|
||||
.strip_prefix("http://")
|
||||
.unwrap_or(url)
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or("127.0.0.1");
|
||||
|
||||
let request = format!(
|
||||
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
||||
path, host
|
||||
);
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
let mut client = client;
|
||||
client
|
||||
.write_all(request.as_bytes())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut response = Vec::new();
|
||||
client
|
||||
.read_to_end(&mut response)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let response_str = String::from_utf8_lossy(&response);
|
||||
let body = response_str
|
||||
.split("\r\n\r\n")
|
||||
.nth(1)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
Ok(body)
|
||||
let resp = reqwest::get(url).await.map_err(|e| e.to_string())?;
|
||||
resp.text().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn read_devtools_active_port(user_data_dir: &Path) -> Option<(u16, String)> {
|
||||
@@ -559,6 +578,7 @@ fn expand_tilde(path: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::EnvGuard;
|
||||
|
||||
#[test]
|
||||
fn test_find_chrome_returns_some_on_host() {
|
||||
@@ -626,10 +646,138 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_playwright_chromium_nonexistent() {
|
||||
// With no Playwright cache, should return None
|
||||
std::env::set_var("PLAYWRIGHT_BROWSERS_PATH", "/nonexistent/path");
|
||||
let _guard = EnvGuard::new(&["PLAYWRIGHT_BROWSERS_PATH"]);
|
||||
_guard.set("PLAYWRIGHT_BROWSERS_PATH", "/nonexistent/path");
|
||||
let result = find_playwright_chromium();
|
||||
std::env::remove_var("PLAYWRIGHT_BROWSERS_PATH");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_headless_includes_headless_flag() {
|
||||
let opts = LaunchOptions {
|
||||
headless: true,
|
||||
..Default::default()
|
||||
};
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
assert!(result.args.iter().any(|a| a == "--headless=new"));
|
||||
assert!(result
|
||||
.args
|
||||
.iter()
|
||||
.any(|a| a == "--window-size=1280,720"));
|
||||
// Temp dir created when no profile
|
||||
assert!(result.temp_user_data_dir.is_some());
|
||||
let dir = result.temp_user_data_dir.unwrap();
|
||||
assert!(dir.exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_headed_no_headless_flag() {
|
||||
let opts = LaunchOptions {
|
||||
headless: false,
|
||||
..Default::default()
|
||||
};
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
assert!(!result.args.iter().any(|a| a.contains("--headless")));
|
||||
assert!(!result.args.iter().any(|a| a.starts_with("--window-size=")));
|
||||
// Temp dir created when no profile
|
||||
assert!(result.temp_user_data_dir.is_some());
|
||||
let dir = result.temp_user_data_dir.unwrap();
|
||||
assert!(dir.exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_temp_user_data_dir_created() {
|
||||
let opts = LaunchOptions::default();
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
let dir = result.temp_user_data_dir.as_ref().unwrap();
|
||||
assert!(dir.exists());
|
||||
assert!(result
|
||||
.args
|
||||
.iter()
|
||||
.any(|a| a.starts_with("--user-data-dir=")));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_profile_no_temp_dir() {
|
||||
let opts = LaunchOptions {
|
||||
profile: Some("/tmp/my-profile".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
assert!(result.temp_user_data_dir.is_none());
|
||||
assert!(result
|
||||
.args
|
||||
.iter()
|
||||
.any(|a| a == "--user-data-dir=/tmp/my-profile"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_custom_window_size_not_overridden() {
|
||||
let opts = LaunchOptions {
|
||||
headless: true,
|
||||
args: vec!["--window-size=1920,1080".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
assert!(!result
|
||||
.args
|
||||
.iter()
|
||||
.any(|a| a == "--window-size=1280,720"));
|
||||
assert!(result
|
||||
.args
|
||||
.iter()
|
||||
.any(|a| a == "--window-size=1920,1080"));
|
||||
if let Some(ref dir) = result.temp_user_data_dir {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_start_maximized_suppresses_default_window_size() {
|
||||
let opts = LaunchOptions {
|
||||
headless: true,
|
||||
args: vec!["--start-maximized".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let result = build_chrome_args(&opts).unwrap();
|
||||
assert!(!result.args.iter().any(|a| a == "--window-size=1280,720"));
|
||||
assert!(result.args.iter().any(|a| a == "--start-maximized"));
|
||||
if let Some(ref dir) = result.temp_user_data_dir {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chrome_process_drop_cleans_temp_dir() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"agent-browser-chrome-drop-test-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
assert!(dir.exists());
|
||||
|
||||
{
|
||||
// Simulate a ChromeProcess with a temp dir but a dummy child.
|
||||
// We can't actually spawn Chrome here, but we can verify the Drop
|
||||
// logic by creating a small helper process.
|
||||
let child = Command::new("echo")
|
||||
.arg("test")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let _process = ChromeProcess {
|
||||
child,
|
||||
ws_url: String::new(),
|
||||
temp_user_data_dir: Some(dir.clone()),
|
||||
};
|
||||
// _process dropped here
|
||||
}
|
||||
|
||||
assert!(!dir.exists(), "Temp dir should be cleaned up on drop");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ impl ActionPolicy {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::EnvGuard;
|
||||
|
||||
#[test]
|
||||
fn test_policy_allow_whitelist() {
|
||||
@@ -205,12 +206,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_confirm_actions_from_env() {
|
||||
env::set_var("AGENT_BROWSER_CONFIRM_ACTIONS", "navigate,click,fill");
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_CONFIRM_ACTIONS"]);
|
||||
_guard.set("AGENT_BROWSER_CONFIRM_ACTIONS", "navigate,click,fill");
|
||||
let ca = ConfirmActions::from_env().unwrap();
|
||||
assert!(ca.requires_confirmation("navigate"));
|
||||
assert!(ca.requires_confirmation("click"));
|
||||
assert!(ca.requires_confirmation("fill"));
|
||||
assert!(!ca.requires_confirmation("screenshot"));
|
||||
env::remove_var("AGENT_BROWSER_CONFIRM_ACTIONS");
|
||||
}
|
||||
}
|
||||
|
||||
+8
-68
@@ -871,7 +871,6 @@ Aliases: goto, navigate
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
--headers <json> Set HTTP headers (scoped to this origin)
|
||||
--risk-mode <mode> Risk handling for verify/captcha pages: off, warn, block
|
||||
--wait-until <mode> Navigation wait strategy: load, domcontentloaded, networkidle
|
||||
@@ -898,7 +897,6 @@ the browser's back button.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser back
|
||||
@@ -915,7 +913,6 @@ the browser's forward button.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser forward
|
||||
@@ -932,7 +929,6 @@ the browser's reload button.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser reload
|
||||
@@ -955,7 +951,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser click "#submit-button"
|
||||
@@ -976,7 +971,6 @@ or triggering double-click handlers.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser dblclick "#editable-text"
|
||||
@@ -994,7 +988,6 @@ This replaces any existing content in the field.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser fill "#email" "user@example.com"
|
||||
@@ -1014,7 +1007,6 @@ Use --delay to add per-character delay (milliseconds).
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser type "#search" "hello"
|
||||
@@ -1039,7 +1031,6 @@ triggering hover states or dropdown menus.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser hover "#dropdown-trigger"
|
||||
@@ -1056,7 +1047,6 @@ Sets keyboard focus to the specified element.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser focus "#input-field"
|
||||
@@ -1073,7 +1063,6 @@ Checks a checkbox element. If already checked, no action is taken.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser check "#terms-checkbox"
|
||||
@@ -1090,7 +1079,6 @@ Unchecks a checkbox element. If already unchecked, no action is taken.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser uncheck "#newsletter-opt-in"
|
||||
@@ -1107,7 +1095,6 @@ Selects one or more options in a <select> dropdown by value.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser select "#country" "US"
|
||||
@@ -1125,7 +1112,6 @@ Drags an element from source to target location.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser drag "#draggable" "#drop-zone"
|
||||
@@ -1142,7 +1128,6 @@ Uploads one or more files to a file input element.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser upload "#file-input" ./document.pdf
|
||||
@@ -1163,7 +1148,6 @@ Arguments:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser download "#download-btn" ./file.pdf
|
||||
@@ -1194,7 +1178,6 @@ Modifiers (combine with +):
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser press Enter
|
||||
@@ -1215,7 +1198,6 @@ Useful for holding modifier keys.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser keydown Shift
|
||||
@@ -1232,7 +1214,6 @@ Releases a key that was pressed with keydown.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser keyup Shift
|
||||
@@ -1260,7 +1241,6 @@ directly — it already operates on the current focus.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser keyboard type "Hello, World!"
|
||||
@@ -1295,7 +1275,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser scroll
|
||||
@@ -1317,7 +1296,6 @@ Aliases: scrollinto
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser scrollintoview "#footer"
|
||||
@@ -1349,7 +1327,6 @@ Download Options (with --download):
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser wait "#loading-spinner"
|
||||
@@ -1383,7 +1360,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser screenshot
|
||||
@@ -1404,7 +1380,6 @@ Saves the current page as a PDF file.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser pdf ./page.pdf
|
||||
@@ -1432,7 +1407,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser snapshot
|
||||
@@ -1458,7 +1432,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser eval "document.title"
|
||||
@@ -1487,11 +1460,9 @@ Aliases: quit, exit
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser close
|
||||
agent-browser close --session mysession
|
||||
"##
|
||||
}
|
||||
|
||||
@@ -1517,7 +1488,6 @@ Subcommands:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser get text @e1
|
||||
@@ -1549,7 +1519,6 @@ Subcommands:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser is visible "#modal"
|
||||
@@ -1588,7 +1557,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser find role button click --name Submit
|
||||
@@ -1618,7 +1586,6 @@ Subcommands:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser mouse move 100 200
|
||||
@@ -1651,7 +1618,6 @@ Settings:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser set viewport 1920 1080
|
||||
@@ -1685,7 +1651,6 @@ Subcommands:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser network route "**/api/*" --abort
|
||||
@@ -1717,7 +1682,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser storage local
|
||||
@@ -1757,7 +1721,6 @@ When --url is omitted, --domain and --path must be provided together.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
# Simple cookie for current page
|
||||
@@ -1800,7 +1763,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser tab
|
||||
@@ -1827,7 +1789,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser window new
|
||||
@@ -1849,7 +1810,6 @@ Arguments:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser frame "#embed-iframe"
|
||||
@@ -1883,7 +1843,6 @@ Save Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
@@ -1931,7 +1890,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser dialog accept
|
||||
@@ -1955,7 +1913,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser trace start
|
||||
@@ -1986,7 +1943,6 @@ Start Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
# Basic profiling
|
||||
@@ -2025,7 +1981,6 @@ Operations:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
# Record from current page (preserves login state)
|
||||
@@ -2057,7 +2012,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser console
|
||||
@@ -2077,7 +2031,6 @@ Options:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser errors
|
||||
@@ -2096,7 +2049,6 @@ Visually highlights an element on the page for debugging.
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser highlight "#target-element"
|
||||
@@ -2124,7 +2076,7 @@ Operations:
|
||||
|
||||
Automatic State Persistence:
|
||||
Use --session-name to auto-save/restore state across restarts.
|
||||
If omitted, it defaults to --session (or "default"):
|
||||
If omitted, it defaults to "default":
|
||||
agent-browser --session-name myapp open https://example.com
|
||||
Or set AGENT_BROWSER_SESSION_NAME environment variable.
|
||||
|
||||
@@ -2134,7 +2086,6 @@ State Encryption:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser state save ./auth-state.json
|
||||
@@ -2154,24 +2105,18 @@ agent-browser session - Manage sessions
|
||||
|
||||
Usage: agent-browser session [operation]
|
||||
|
||||
Manage isolated browser sessions. Each session has its own browser
|
||||
instance with separate cookies, storage, and state.
|
||||
Show the current fixed session and active daemon state.
|
||||
|
||||
Operations:
|
||||
(none) Show current session name
|
||||
list List all active sessions
|
||||
|
||||
Environment:
|
||||
AGENT_BROWSER_SESSION Default session name
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser session
|
||||
agent-browser session list
|
||||
agent-browser --session test open example.com
|
||||
"##
|
||||
}
|
||||
|
||||
@@ -2214,7 +2159,6 @@ Supported URL formats:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
# Connect to local Chrome with remote debugging
|
||||
@@ -2249,10 +2193,10 @@ Notes:
|
||||
- doctor does not accept positional arguments
|
||||
- If browser is not already connected, doctor will still report CDP probe results
|
||||
- Plugin handshake requires CDP mode, a normal http(s) page, and the extension installed
|
||||
- Extension side panel includes browser controls, developer signal panels, workflow shortcuts, and scheduled automation
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser doctor
|
||||
@@ -2272,7 +2216,6 @@ for touch-based interfaces like iOS Safari.
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser tap "#submit-button"
|
||||
@@ -2295,7 +2238,6 @@ Arguments:
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser -p ios swipe up
|
||||
@@ -2314,7 +2256,6 @@ Subcommands:
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser device list
|
||||
@@ -2369,7 +2310,6 @@ URL Diff:
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser diff snapshot
|
||||
@@ -2497,7 +2437,7 @@ Snapshot Options:
|
||||
-s, --selector <sel> Scope to CSS selector
|
||||
|
||||
Options:
|
||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||
--session <name> Ignored (single default session only)
|
||||
--state <path> Load storage state from JSON file (or AGENT_BROWSER_STATE env)
|
||||
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||
@@ -2516,7 +2456,7 @@ Options:
|
||||
--json JSON output
|
||||
--full, -f Full page screenshot
|
||||
--annotate Annotated screenshot with numbered labels and legend
|
||||
--headed Show browser window (not headless)
|
||||
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
|
||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||
--auto-connect Auto-discover and connect to running Chrome
|
||||
Project default: try localhost:9333 first, then auto-discovery (no managed local-launch fallback)
|
||||
@@ -2524,9 +2464,10 @@ Options:
|
||||
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
|
||||
--tab-group <name> Base title for agent tab groups (CDP plugin mode; silent no-op if plugin unavailable)
|
||||
--tab-group-plugin-id <id> Expected Chrome extension ID for tab-group handshake (or AGENT_BROWSER_TAB_GROUP_PLUGIN_ID)
|
||||
Extension side panel supports browser controls + console/network/DOM + workflow scheduling
|
||||
--risk-mode <mode> Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE)
|
||||
--wait-until <mode> Navigation wait strategy for open/navigate: load, domcontentloaded, networkidle
|
||||
--session-name <name> Auto-save/restore session state (defaults to --session)
|
||||
--session-name <name> Auto-save/restore session state (defaults to "default")
|
||||
--content-boundaries Wrap page output in boundary markers (or AGENT_BROWSER_CONTENT_BOUNDARIES)
|
||||
--max-output <chars> Truncate page output to N chars (or AGENT_BROWSER_MAX_OUTPUT)
|
||||
--allowed-domains <list> Restrict navigation domains (or AGENT_BROWSER_ALLOWED_DOMAINS)
|
||||
@@ -2564,8 +2505,7 @@ Configuration:
|
||||
|
||||
Environment:
|
||||
AGENT_BROWSER_CONFIG Path to config file (or use --config)
|
||||
AGENT_BROWSER_SESSION Session name (default: "default")
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name (default: AGENT_BROWSER_SESSION)
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name (default: "default")
|
||||
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM state encryption
|
||||
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete states older than N days (default: 30)
|
||||
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
/// Global mutex shared across all test modules to prevent parallel tests from
|
||||
/// interfering with each other when mutating environment variables.
|
||||
pub static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// RAII guard that locks [`ENV_MUTEX`] and restores environment variables on drop.
|
||||
pub struct EnvGuard<'a> {
|
||||
_lock: MutexGuard<'a, ()>,
|
||||
vars: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl<'a> EnvGuard<'a> {
|
||||
pub fn new(var_names: &[&str]) -> Self {
|
||||
let lock = ENV_MUTEX.lock().unwrap();
|
||||
let vars = var_names
|
||||
.iter()
|
||||
.map(|&name| (name.to_string(), std::env::var(name).ok()))
|
||||
.collect();
|
||||
Self { _lock: lock, vars }
|
||||
}
|
||||
|
||||
pub fn set(&self, name: &str, value: &str) {
|
||||
debug_assert!(
|
||||
self.vars.iter().any(|(n, _)| n == name),
|
||||
"EnvGuard::set called with unregistered var: {name}"
|
||||
);
|
||||
std::env::set_var(name, value);
|
||||
}
|
||||
|
||||
pub fn remove(&self, name: &str) {
|
||||
debug_assert!(
|
||||
self.vars.iter().any(|(n, _)| n == name),
|
||||
"EnvGuard::remove called with unregistered var: {name}"
|
||||
);
|
||||
std::env::remove_var(name);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
for (name, value) in &self.vars {
|
||||
match value {
|
||||
Some(v) => std::env::set_var(name, v),
|
||||
None => std::env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "7cb6c7d950c040b2198da553140e1b5e8b6ac682"
|
||||
},
|
||||
"path_in_vcs": "crates/zune-jpeg"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
# Benchmarks of popular jpeg libraries
|
||||
|
||||
Here I compare how long it takes popular JPEG decoders to decode the below 7680*4320 image
|
||||
of (now defunct ?) [Cutefish OS](https://en.cutefishos.com/) default wallpaper.
|
||||

|
||||
|
||||
## About benchmarks
|
||||
|
||||
Benchmarks are weird, especially IO & multi-threaded programs. This library uses both of the above hence performance may
|
||||
vary.
|
||||
|
||||
For best results shut down your machine, go take coffee, think about life and how it came to be and why people should
|
||||
save the environment.
|
||||
|
||||
Then power up your machine, if it's a laptop connect it to a power supply and if there is a setting for performance
|
||||
mode, tweak it.
|
||||
|
||||
Then run.
|
||||
|
||||
## Benchmarks vs real world usage
|
||||
|
||||
Real world usage may vary.
|
||||
|
||||
Notice that I'm using a large image but probably most decoding will be small to medium images.
|
||||
|
||||
To make the library thread safe, we do about 1.5-1.7x more allocations than libjpeg-turbo. Although, do note that the
|
||||
allocations do not occur at ago, we allocate when needed and deallocate when not needed.
|
||||
|
||||
Do note if memory bandwidth is a limitation. This is not for you.
|
||||
|
||||
## Reproducibility
|
||||
|
||||
The benchmarks are carried out on my local machine with an AMD Ryzen 5 4500u
|
||||
|
||||
The benchmarks are reproducible.
|
||||
|
||||
To reproduce them
|
||||
|
||||
1. Clone this repository
|
||||
2. Install rust(if you don't have it yet)
|
||||
3. `cd` into the directory.
|
||||
4. Run `cargo bench`
|
||||
|
||||
## Performance features of the three libraries
|
||||
|
||||
| feature | image-rs/jpeg-decoder | libjpeg-turbo | zune-jpeg |
|
||||
|------------------------------|-----------------------|---------------|-----------|
|
||||
| multithreaded | ✅ | ❌ | ❌ |
|
||||
| platform specific intrinsics | ✅ | ✅ | ✅ |
|
||||
|
||||
- Image-rs/jpeg-decoder uses [rayon] under the hood but it's under a feature
|
||||
flag.
|
||||
|
||||
- libjpeg-turbo uses hand-written asm for platform specific intrinsics, ported to
|
||||
the most common architectures out there but falls back to scalar
|
||||
code if it can't run in a platform.
|
||||
|
||||
# Finally benchmarks
|
||||
|
||||
[here]
|
||||
|
||||
## Notes
|
||||
|
||||
Benchmarks are ran at least once a week to catch regressions early and
|
||||
are uploaded to Github pages.
|
||||
|
||||
Machine specs can be found on the other [landing page]
|
||||
|
||||
Benchmarks may not reflect real world usage(threads, other I/O machine bottlenecks)
|
||||
|
||||
[landing page]:https://etemesi254.github.io/posts/Zune-Benchmarks/
|
||||
|
||||
[here]:https://etemesi254.github.io/assets/criterion/report/index.html
|
||||
|
||||
[libjpeg-turbo]:https://github.com/libjpeg-turbo/libjpeg-turbo
|
||||
|
||||
[jpeg-decoder]:https://github.com/image-rs/jpeg-decoder
|
||||
|
||||
[rayon]:https://github.com/rayon-rs/rayon
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.75.0"
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
authors = ["caleb <etemesicaleb@gmail.com>"]
|
||||
build = false
|
||||
exclude = [
|
||||
"/benches/images/*",
|
||||
"/tests/*",
|
||||
"/.idea/*",
|
||||
"/.gradle/*",
|
||||
"/test-images/*",
|
||||
"fuzz/*",
|
||||
]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A fast, correct and safe jpeg decoder"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"jpeg",
|
||||
"jpeg-decoder",
|
||||
"decoder",
|
||||
]
|
||||
categories = ["multimedia::images"]
|
||||
license = "MIT OR Apache-2.0 OR Zlib"
|
||||
repository = "https://github.com/etemesi254/zune-image/tree/dev/crates/zune-jpeg"
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"x86",
|
||||
"neon",
|
||||
"std",
|
||||
]
|
||||
log = ["zune-core/log"]
|
||||
neon = []
|
||||
portable_simd = []
|
||||
std = ["zune-core/std"]
|
||||
x86 = []
|
||||
|
||||
[lib]
|
||||
name = "zune_jpeg"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.zune-core]
|
||||
version = "0.5.1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "warn"
|
||||
priority = 0
|
||||
check-cfg = ["cfg(fuzzing)"]
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
rust-version = "1.75.0"
|
||||
authors = ["caleb <etemesicaleb@gmail.com>"]
|
||||
edition = "2021"
|
||||
repository = "https://github.com/etemesi254/zune-image/tree/dev/crates/zune-jpeg"
|
||||
license = "MIT OR Apache-2.0 OR Zlib"
|
||||
keywords = ["jpeg", "jpeg-decoder", "decoder"]
|
||||
categories = ["multimedia::images"]
|
||||
exclude = ["/benches/images/*", "/tests/*", "/.idea/*", "/.gradle/*", "/test-images/*", "fuzz/*"]
|
||||
description = "A fast, correct and safe jpeg decoder"
|
||||
|
||||
[lints.rust]
|
||||
# Disable feature checker for fuzzing since it's used and cargo doesn't
|
||||
# seem to recognise fuzzing
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
x86 = []
|
||||
neon = []
|
||||
std = ["zune-core/std"]
|
||||
# NOTE: portable_simd requires Rust 1.87+
|
||||
portable_simd = []
|
||||
log = ["zune-core/log"]
|
||||
default = ["x86", "neon", "std"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
zune-core = { path = "../zune-core", version = "0.5.1" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
zune-ppm = { path = "../zune-ppm" }
|
||||
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
## Version 0.5.7
|
||||
- Move scalar idct to wrapping maths.
|
||||
- Simd upsampling (mhils)
|
||||
- Faster zero idct check (mhils)
|
||||
## Version 0.5.6
|
||||
- Better support for truncated images (by https://github.com/mhils)
|
||||
- fix 4:1:0 chroma subsampling (by https://github.com/mhils)
|
||||
- Fix some crashes
|
||||
- Fix some bug on last pixel sampling
|
||||
|
||||
## Version 0.5.5
|
||||
- Support direct conversion of Luma to RGBA
|
||||
## Version 0.5.4
|
||||
- Fix overriding color space when decoding Luma colorspace
|
||||
## Version 0.5.3
|
||||
- Fix some decoding of some images with markers in progressive segments, see https://github.com/etemesi254/zune-image/issues/295
|
||||
## Version 0.5.1
|
||||
|
||||
- Fix decoding of particular images with a non-standard subsample, (
|
||||
see https://github.com/etemesi254/zune-image/issues/291)
|
||||
- Add better RGB color detection of images to match libjpeg and stb_image formats
|
||||
|
||||
-----
|
||||
|
||||
## Version 0.3.17
|
||||
|
||||
- Fix no-std compilation
|
||||
|
||||
## Version 0.3.16
|
||||
|
||||
- Add support for decoding to BGR and BGRA
|
||||
|
||||
## Version 0.3.14
|
||||
|
||||
- Add ability to parse exif and ICC chunk.
|
||||
- Fix images with one component that were down-sampled.
|
||||
|
||||
### Version 0.3.13
|
||||
|
||||
- Allow decoding into pre-allocated buffer
|
||||
- Clarify documentation
|
||||
|
||||
### Version 0.3.11
|
||||
|
||||
- Add guards for SSE and AVX code paths(allows compiling for platforms that do not support it)
|
||||
|
||||
### Version 0.3.0
|
||||
|
||||
- Overhaul to the whole decoder.
|
||||
- Single threaded version
|
||||
- Lightweight.
|
||||
|
||||
### Version 0.2.0
|
||||
|
||||
- New `ZuneJpegOptions` struct, this is the now recommended way to set up decoding options for
|
||||
decoding
|
||||
- Deprecated previous options setting functions.
|
||||
- More code cleanups
|
||||
- Fixed new bugs discovered by fuzzing
|
||||
- Removed dependency on `num_cpu`
|
||||
|
||||
### Version 0.1.5
|
||||
|
||||
- Allow user to set memory limits in during decoding explicitly via `set_limits`
|
||||
- Fixed some bugs discovered by fuzzing
|
||||
- Correctly handle small images less than 16 pixels
|
||||
- Gracefully handle incorrectly sampled images.
|
||||
|
||||
### Version 0.1.4
|
||||
|
||||
- Remove all `unsafe` instances except platform dependent intrinsics.
|
||||
- Numerous bug fixes identified by fuzzing.
|
||||
- Expose `ImageInfo` to the crate root.
|
||||
|
||||
### Version 0.1.3
|
||||
|
||||
- Fix numerous panics found by fuzzing(thanks to @[Shnatsel] for the corpus)
|
||||
- Add new method `set_num_threads` that allows one to explicitly set the number of threads to use to decode the image.
|
||||
|
||||
### Version 0.1.2
|
||||
|
||||
- Add more sub checks, contributed by @[5225225]
|
||||
- Privatize some modules.
|
||||
|
||||
### Version 0.1.1
|
||||
|
||||
- Fix rgba/rgbx decoding when avx optimized functions were used
|
||||
- Initial support for fuzzing
|
||||
- Remove `align_alloc` method which was unsound (Thanks to @[HeroicKatora] for pointing that out)
|
||||
|
||||
[Shnatsel]:https://github.com/Shnatsel
|
||||
|
||||
[HeroicKatora]:https://github.com/HeroicKatora
|
||||
|
||||
[5225225]:https://github.com/5225225
|
||||
Vendored
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) zune-image developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
zlib License
|
||||
|
||||
(C) zune-image developers
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
# Zune-JPEG
|
||||
|
||||
A fast, correct and safe jpeg decoder in pure Rust.
|
||||
|
||||
## Usage
|
||||
|
||||
The library provides a simple-to-use API for jpeg decoding
|
||||
and an ability to add options to influence decoding.
|
||||
|
||||
### Example
|
||||
|
||||
```Rust
|
||||
// Import the library
|
||||
use zune_jpeg::JpegDecoder;
|
||||
use std::fs::read;
|
||||
|
||||
fn main()->Result<(),DecoderErrors> {
|
||||
// load some jpeg data
|
||||
let data = read("cat.jpg").unwrap();
|
||||
// create a decoder
|
||||
let mut decoder = JpegDecoder::new(&data);
|
||||
// decode the file
|
||||
let pixels = decoder.decode()?;
|
||||
}
|
||||
```
|
||||
|
||||
The decoder supports more manipulations via `DecoderOptions`,
|
||||
see additional documentation in the library.
|
||||
|
||||
## Goals
|
||||
|
||||
The implementation aims to have the following goals achieved,
|
||||
in order of importance
|
||||
|
||||
1. Safety - Do not segfault on errors or invalid input. Panics are okay, but
|
||||
should be fixed when reported. `unsafe` is only used for SIMD intrinsics,
|
||||
and can be turned off entirely both at compile time and at runtime.
|
||||
2. Speed - Get the data as quickly as possible, which means
|
||||
1. Platform intrinsics code where justifiable
|
||||
2. Carefully written platform independent code that allows the
|
||||
compiler to vectorize it.
|
||||
3. Regression tests.
|
||||
4. Watch the memory usage of the program
|
||||
3. Usability - Provide utility functions like different color conversions functions.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Bit identical results with libjpeg/libjpeg-turbo will never be an aim of this library.
|
||||
Jpeg is a lossy format with very few parts specified by the standard
|
||||
(i.e it doesn't give a reference upsampling and color conversion algorithm)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] A Pretty fast 8*8 integer IDCT.
|
||||
- [x] Fast Huffman Decoding
|
||||
- [x] Fast color convert functions.
|
||||
- [x] Support for extended colorspaces like GrayScale and RGBA
|
||||
- [X] Single-threaded decoding.
|
||||
- [X] Support for four component JPEGs, and esoteric color schemes like CYMK
|
||||
- [X] Support for `no_std`
|
||||
- [X] BGR/BGRA decoding support.
|
||||
|
||||
## Crate Features
|
||||
|
||||
| feature | on | Capabilities |
|
||||
|---------|-----|---------------------------------------------------------------------------------------------|
|
||||
| `x86` | yes | Enables `x86` specific instructions, specifically `avx` and `sse` for accelerated decoding. |
|
||||
| `std` | yes | Enable linking to the `std` crate |
|
||||
|
||||
Note that the `x86` features are automatically disabled on platforms that aren't x86 during compile
|
||||
time hence there is no need to disable them explicitly if you are targeting such a platform.
|
||||
|
||||
## Using in a `no_std` environment
|
||||
|
||||
The crate can be used in a `no_std` environment with the `alloc` feature.
|
||||
|
||||
But one is required to link to a working allocator for whatever environment the decoder
|
||||
will be running on
|
||||
|
||||
## Debug vs release
|
||||
|
||||
The decoder heavily relies on platform specific intrinsics, namely AVX2 and SSE to gain speed-ups in decoding,
|
||||
but they [perform poorly](https://godbolt.org/z/vPq57z13b) in debug builds. To get reasonable performance even
|
||||
when compiling your program in debug mode, add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# `zune-jpeg` package will be always built with optimizations
|
||||
[profile.dev.package.zune-jpeg]
|
||||
opt-level = 3
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The library tries to be at fast as [libjpeg-turbo] while being as safe as possible.
|
||||
Platform specific intrinsics help get speed up intensive operations ensuring we can almost
|
||||
match [libjpeg-turbo] speeds but speeds are always +- 10 ms of this library.
|
||||
|
||||
For more up-to-date benchmarks, see the online repo with
|
||||
benchmarks [here](https://etemesi254.github.io/assets/criterion/report/index.html)
|
||||
|
||||
|
||||
[libjpeg-turbo]:https://github.com/libjpeg-turbo/libjpeg-turbo/
|
||||
|
||||
[image-rs/jpeg-decoder]:https://github.com/image-rs/jpeg-decoder/tree/master/src
|
||||
+811
@@ -0,0 +1,811 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![allow(
|
||||
clippy::if_not_else,
|
||||
clippy::similar_names,
|
||||
clippy::inline_always,
|
||||
clippy::doc_markdown,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
|
||||
//! This file exposes a single struct that can decode a huffman encoded
|
||||
//! Bitstream in a JPEG file
|
||||
//!
|
||||
//! This code is optimized for speed.
|
||||
//! It's meant to be super duper super fast, because everyone else depends on this being fast.
|
||||
//! It's (annoyingly) serial hence we cant use parallel bitstreams(it's variable length coding.)
|
||||
//!
|
||||
//! Furthermore, on the case of refills, we have to do bytewise processing because the standard decided
|
||||
//! that we want to support markers in the middle of streams(seriously few people use RST markers).
|
||||
//!
|
||||
//! So we pull in all optimization steps:
|
||||
//! - use `inline[always]`? ✅ ,
|
||||
//! - pre-execute most common cases ✅,
|
||||
//! - add random comments ✅
|
||||
//! - fast paths ✅.
|
||||
//!
|
||||
//! Speed-wise: It is probably the fastest JPEG BitStream decoder to ever sail the seven seas because of
|
||||
//! a couple of optimization tricks.
|
||||
//! 1. Fast refills from libjpeg-turbo
|
||||
//! 2. As few as possible branches in decoder fast paths.
|
||||
//! 3. Accelerated AC table decoding borrowed from stb_image.h written by Fabian Gissen (@ rygorous),
|
||||
//! improved by me to handle more cases.
|
||||
//! 4. Safe and extensible routines(e.g. cool ways to eliminate bounds check)
|
||||
//! 5. No unsafe here
|
||||
//!
|
||||
//! Readability comes as a second priority(I tried with variable names this time, and we are wayy better than libjpeg).
|
||||
//!
|
||||
//! Anyway if you are reading this it means your cool and I hope you get whatever part of the code you are looking for
|
||||
//! (or learn something cool)
|
||||
//!
|
||||
//! Knock yourself out.
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
use core::cmp::min;
|
||||
|
||||
use zune_core::bytestream::{ZByteReaderTrait, ZReader};
|
||||
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::huffman::{HuffmanTable, HUFF_LOOKAHEAD};
|
||||
use crate::marker::Marker;
|
||||
use crate::mcu::DCT_BLOCK;
|
||||
use crate::misc::UN_ZIGZAG;
|
||||
|
||||
macro_rules! decode_huff {
|
||||
($stream:tt,$symbol:tt,$table:tt) => {
|
||||
let mut code_length = $symbol >> HUFF_LOOKAHEAD;
|
||||
|
||||
($symbol) &= (1 << HUFF_LOOKAHEAD) - 1;
|
||||
|
||||
if code_length > i32::from(HUFF_LOOKAHEAD)
|
||||
{
|
||||
// if the symbol cannot be resolved in the first HUFF_LOOKAHEAD bits,
|
||||
// we know it lies somewhere between HUFF_LOOKAHEAD and 16 bits since jpeg imposes 16 bit
|
||||
// limit, we can therefore look 16 bits ahead and try to resolve the symbol
|
||||
// starting from 1+HUFF_LOOKAHEAD bits.
|
||||
$symbol = ($stream).peek_bits::<16>() as i32;
|
||||
// (Credits to Sean T. Barrett stb library for this optimization)
|
||||
// maxcode is pre-shifted 16 bytes long so that it has (16-code_length)
|
||||
// zeroes at the end hence we do not need to shift in the inner loop.
|
||||
while code_length < 17{
|
||||
if $symbol < $table.maxcode[code_length as usize] {
|
||||
break;
|
||||
}
|
||||
code_length += 1;
|
||||
}
|
||||
|
||||
if code_length == 17{
|
||||
// symbol could not be decoded.
|
||||
//
|
||||
// We may think, lets fake zeroes, noo
|
||||
// panic, because Huffman codes are sensitive, probably everything
|
||||
// after this will be corrupt, so no need to continue.
|
||||
// panic!("Bad Huffman code length");
|
||||
return Err(DecodeErrors::Format(format!("Bad Huffman Code 0x{:X}, corrupt JPEG",$symbol)))
|
||||
}
|
||||
|
||||
$symbol >>= (16-code_length);
|
||||
($symbol) = i32::from(
|
||||
($table).values
|
||||
[(($symbol + ($table).offset[code_length as usize]) & 0xFF) as usize],
|
||||
);
|
||||
}
|
||||
if code_length> i32::from(($stream).bits_left){
|
||||
return Err(DecodeErrors::Format(format!("Code length {code_length} more than bits left {}",($stream).bits_left)))
|
||||
}
|
||||
// drop bits read
|
||||
($stream).drop_bits(code_length as u8);
|
||||
};
|
||||
}
|
||||
|
||||
/// A `BitStream` struct, a bit by bit reader with super powers
|
||||
///
|
||||
#[rustfmt::skip]
|
||||
pub(crate) struct BitStream {
|
||||
/// A MSB type buffer that is used for some certain operations
|
||||
pub buffer: u64,
|
||||
/// A TOP aligned MSB type buffer that is used to accelerate some operations like
|
||||
/// peek_bits and get_bits.
|
||||
///
|
||||
/// By top aligned, I mean the top bit (63) represents the top bit in the buffer.
|
||||
aligned_buffer: u64,
|
||||
/// Tell us the bits left the two buffer
|
||||
pub(crate) bits_left: u8,
|
||||
/// Did we find a marker(RST/EOF) during decoding?
|
||||
pub marker: Option<Marker>,
|
||||
/// An i16 with the bit corresponding to successive_low set to 1, others 0.
|
||||
pub successive_low_mask: i16,
|
||||
spec_start: u8,
|
||||
spec_end: u8,
|
||||
pub eob_run: i32,
|
||||
pub overread_by: usize,
|
||||
/// True if we have seen end of image marker.
|
||||
/// Don't read anything after that.
|
||||
pub seen_eoi: bool,
|
||||
}
|
||||
|
||||
impl BitStream {
|
||||
/// Create a new BitStream
|
||||
#[rustfmt::skip]
|
||||
pub(crate) const fn new() -> BitStream {
|
||||
BitStream {
|
||||
buffer: 0,
|
||||
aligned_buffer: 0,
|
||||
bits_left: 0,
|
||||
marker: None,
|
||||
successive_low_mask: 1,
|
||||
spec_start: 0,
|
||||
spec_end: 0,
|
||||
eob_run: 0,
|
||||
overread_by: 0,
|
||||
seen_eoi: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Bitstream for progressive decoding
|
||||
#[allow(clippy::redundant_field_names)]
|
||||
#[rustfmt::skip]
|
||||
pub(crate) fn new_progressive(al: u8, spec_start: u8, spec_end: u8) -> BitStream {
|
||||
BitStream {
|
||||
buffer: 0,
|
||||
aligned_buffer: 0,
|
||||
bits_left: 0,
|
||||
marker: None,
|
||||
successive_low_mask: 1i16 << al,
|
||||
spec_start: spec_start,
|
||||
spec_end: spec_end,
|
||||
eob_run: 0,
|
||||
overread_by: 0,
|
||||
seen_eoi: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Refill the bit buffer by (a maximum of) 32 bits
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `reader`:`&mut BufReader<R>`: A mutable reference to an underlying
|
||||
/// File/Memory buffer containing a valid JPEG stream
|
||||
///
|
||||
/// This function will only refill if `self.count` is less than 32
|
||||
#[inline(always)] // to many call sites? ( perf improvement by 4%)
|
||||
pub fn refill<T>(&mut self, reader: &mut ZReader<T>) -> Result<bool, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
/// Macro version of a single byte refill.
|
||||
/// Arguments
|
||||
/// buffer-> our io buffer, because rust macros cannot get values from
|
||||
/// the surrounding environment bits_left-> number of bits left
|
||||
/// to full refill
|
||||
macro_rules! refill {
|
||||
($buffer:expr,$byte:expr,$bits_left:expr) => {
|
||||
// read a byte from the stream
|
||||
$byte = u64::from(reader.read_u8());
|
||||
self.overread_by += usize::from(reader.eof()?);
|
||||
// append to the buffer
|
||||
// JPEG is a MSB type buffer so that means we append this
|
||||
// to the lower end (0..8) of the buffer and push the rest bits above..
|
||||
$buffer = ($buffer << 8) | $byte;
|
||||
// Increment bits left
|
||||
$bits_left += 8;
|
||||
// Check for special case of OxFF, to see if it's a stream or a marker
|
||||
if $byte == 0xff {
|
||||
// read next byte
|
||||
let mut next_byte = u64::from(reader.read_u8());
|
||||
// Byte snuffing, if we encounter byte snuff, we skip the byte
|
||||
if next_byte != 0x00 {
|
||||
// skip that byte we read
|
||||
while next_byte == 0xFF {
|
||||
next_byte = u64::from(reader.read_u8());
|
||||
}
|
||||
|
||||
if next_byte != 0x00 {
|
||||
// Undo the byte append and return
|
||||
$buffer >>= 8;
|
||||
$bits_left -= 8;
|
||||
|
||||
if $bits_left != 0 {
|
||||
self.aligned_buffer = $buffer << (64 - $bits_left);
|
||||
}
|
||||
|
||||
let marker = Marker::from_u8(next_byte as u8);
|
||||
self.marker = marker;
|
||||
|
||||
if let Some(Marker::UNKNOWN(_)) = marker{
|
||||
return Err(DecodeErrors::Format("Unknown marker in bit stream".to_string()));
|
||||
}
|
||||
if next_byte == 0xD9 {
|
||||
// special handling for eoi, fill some bytes,even if its zero,
|
||||
// removes some panics
|
||||
self.buffer <<= 8;
|
||||
self.bits_left += 8;
|
||||
self.aligned_buffer = self.buffer << (64 - self.bits_left);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 32 bits is enough for a decode(16 bits) and receive_extend(max 16 bits)
|
||||
if self.bits_left < 32 {
|
||||
if self.marker.is_some() || self.overread_by > 0 || self.seen_eoi {
|
||||
// found a marker, or we are in EOI
|
||||
// also we are in over-reading mode, where we fill it with zeroes
|
||||
|
||||
// fill with zeroes
|
||||
self.buffer <<= 32;
|
||||
self.bits_left += 32;
|
||||
self.aligned_buffer = self.buffer << (64 - self.bits_left);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// we optimize for the case where we don't have 255 in the stream and have 4 bytes left
|
||||
// as it is the common case
|
||||
//
|
||||
// so we always read 4 bytes, if read_fixed_bytes errors out, the cursor is
|
||||
// guaranteed not to advance in case of failure (is this true), so
|
||||
// we revert the read later on (if we have 255), if this fails, we use the normal
|
||||
// byte at a time read
|
||||
|
||||
if let Ok(bytes) = reader.read_fixed_bytes_or_error::<4>() {
|
||||
// we have 4 bytes to spare, read the 4 bytes into a temporary buffer
|
||||
// create buffer
|
||||
let msb_buf = u32::from_be_bytes(bytes);
|
||||
// check if we have 0xff
|
||||
if !has_byte(msb_buf, 255) {
|
||||
self.bits_left += 32;
|
||||
self.buffer <<= 32;
|
||||
self.buffer |= u64::from(msb_buf);
|
||||
self.aligned_buffer = self.buffer << (64 - self.bits_left);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
reader.rewind(4)?;
|
||||
}
|
||||
// This serves two reasons,
|
||||
// 1: Make clippy shut up
|
||||
// 2: Favour register reuse
|
||||
let mut byte;
|
||||
// 4 refills, if all succeed the stream should contain enough bits to decode a
|
||||
// value
|
||||
refill!(self.buffer, byte, self.bits_left);
|
||||
refill!(self.buffer, byte, self.bits_left);
|
||||
refill!(self.buffer, byte, self.bits_left);
|
||||
refill!(self.buffer, byte, self.bits_left);
|
||||
// Construct an MSB buffer whose top bits are the bitstream we are currently holding.
|
||||
self.aligned_buffer = self.buffer << (64 - self.bits_left);
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
/// Decode the DC coefficient in a MCU block.
|
||||
///
|
||||
/// The decoded coefficient is written to `dc_prediction`
|
||||
///
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::unwrap_used
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn decode_dc<T>(
|
||||
&mut self, reader: &mut ZReader<T>, dc_table: &HuffmanTable, dc_prediction: &mut i32
|
||||
) -> Result<bool, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
let (mut symbol, r);
|
||||
|
||||
if self.bits_left < 32 {
|
||||
self.refill(reader)?;
|
||||
};
|
||||
// look a head HUFF_LOOKAHEAD bits into the bitstream
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
symbol = dc_table.lookup[symbol as usize];
|
||||
|
||||
decode_huff!(self, symbol, dc_table);
|
||||
|
||||
if symbol != 0 {
|
||||
r = self.get_bits(symbol as u8);
|
||||
symbol = huff_extend(r, symbol);
|
||||
}
|
||||
// Update DC prediction
|
||||
*dc_prediction = dc_prediction.wrapping_add(symbol);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
/// Like `decode_dc` but we do not need the result of the component, we only want to remove it
|
||||
/// from the bitstream of the MCU.
|
||||
fn discard_dc<T>(
|
||||
&mut self, reader: &mut ZReader<T>, dc_table: &HuffmanTable
|
||||
) -> Result<bool, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
let mut symbol;
|
||||
|
||||
if self.bits_left < 32 {
|
||||
self.refill(reader)?;
|
||||
};
|
||||
// look a head HUFF_LOOKAHEAD bits into the bitstream
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
symbol = dc_table.lookup[symbol as usize];
|
||||
|
||||
decode_huff!(self, symbol, dc_table);
|
||||
|
||||
if symbol != 0 {
|
||||
let _ = self.get_bits(symbol as u8);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
/// Decode a Minimum Code Unit(MCU) as quickly as possible
|
||||
///
|
||||
/// # Arguments
|
||||
/// - reader: The bitstream from where we read more bits.
|
||||
/// - dc_table: The Huffman table used to decode the DC coefficient
|
||||
/// - ac_table: The Huffman table used to decode AC values
|
||||
/// - block: A memory region where we will write out the decoded values
|
||||
/// - DC prediction: Last DC value for this component
|
||||
///
|
||||
#[allow(
|
||||
clippy::many_single_char_names,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
#[inline(never)]
|
||||
pub fn decode_mcu_block<T>(
|
||||
&mut self, reader: &mut ZReader<T>, dc_table: &HuffmanTable, ac_table: &HuffmanTable,
|
||||
qt_table: &[i32; DCT_BLOCK], block: &mut [i32; 64], dc_prediction: &mut i32
|
||||
) -> Result<u16, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
// Get fast AC table as a reference before we enter the hot path
|
||||
let ac_lookup = ac_table.ac_lookup.as_ref().unwrap();
|
||||
|
||||
let (mut symbol, mut r, mut fast_ac);
|
||||
// Decode AC coefficients
|
||||
let mut pos: usize = 1;
|
||||
if self.bits_left < 1 && self.marker.is_some() {
|
||||
return Err(DecodeErrors::Format(
|
||||
"No more bytes left in stream before marker".to_string()
|
||||
));
|
||||
}
|
||||
// decode DC, dc prediction will contain the value
|
||||
self.decode_dc(reader, dc_table, dc_prediction)?;
|
||||
|
||||
// set dc to be the dc prediction.
|
||||
block[0] = *dc_prediction * qt_table[0];
|
||||
|
||||
while pos < 64 {
|
||||
self.refill(reader)?;
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
fast_ac = ac_lookup[symbol as usize];
|
||||
symbol = ac_table.lookup[symbol as usize];
|
||||
|
||||
if fast_ac != 0 {
|
||||
// FAST AC path
|
||||
pos += ((fast_ac >> 4) & 15) as usize; // run
|
||||
let t_pos = UN_ZIGZAG[min(pos, 63)] & 63;
|
||||
|
||||
block[t_pos] = i32::from(fast_ac >> 8) * (qt_table[t_pos]); // Value
|
||||
self.drop_bits((fast_ac & 15) as u8);
|
||||
pos += 1;
|
||||
} else {
|
||||
decode_huff!(self, symbol, ac_table);
|
||||
|
||||
r = symbol >> 4;
|
||||
symbol &= 15;
|
||||
|
||||
if symbol != 0 {
|
||||
pos += r as usize;
|
||||
r = self.get_bits(symbol as u8);
|
||||
symbol = huff_extend(r, symbol);
|
||||
let t_pos = UN_ZIGZAG[pos & 63] & 63;
|
||||
|
||||
block[t_pos] = symbol * qt_table[t_pos];
|
||||
|
||||
pos += 1;
|
||||
} else if r != 15 {
|
||||
return Ok(pos as u16);
|
||||
} else {
|
||||
pos += 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(64);
|
||||
}
|
||||
|
||||
/// Advance the bitstream over a block but ignore the data contained.
|
||||
///
|
||||
/// This updates DC prediction but we never dequantize and we never do any Zig-Zag translation
|
||||
/// either. Still returns the index of the last component read.
|
||||
pub fn discard_mcu_block<T>(
|
||||
&mut self, reader: &mut ZReader<T>, dc_table: &HuffmanTable, ac_table: &HuffmanTable
|
||||
) -> Result<u16, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
// Get fast AC table as a reference before we enter the hot path
|
||||
let ac_lookup = ac_table.ac_lookup.as_ref().unwrap();
|
||||
|
||||
let (mut symbol, mut r, mut fast_ac);
|
||||
// Decode AC coefficients
|
||||
let mut pos: usize = 1;
|
||||
|
||||
// decode DC, dc prediction will contain the value
|
||||
self.discard_dc(reader, dc_table)?;
|
||||
|
||||
while pos < 64 {
|
||||
self.refill(reader)?;
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
fast_ac = ac_lookup[symbol as usize];
|
||||
symbol = ac_table.lookup[symbol as usize];
|
||||
|
||||
if fast_ac != 0 {
|
||||
// FAST AC path
|
||||
pos += ((fast_ac >> 4) & 15) as usize; // run
|
||||
|
||||
self.drop_bits((fast_ac & 15) as u8);
|
||||
pos += 1;
|
||||
} else {
|
||||
decode_huff!(self, symbol, ac_table);
|
||||
|
||||
r = symbol >> 4;
|
||||
symbol &= 15;
|
||||
|
||||
if symbol != 0 {
|
||||
pos += r as usize;
|
||||
// Advance over bits but ignore.
|
||||
let _ = self.get_bits(symbol as u8);
|
||||
|
||||
pos += 1;
|
||||
} else if r != 15 {
|
||||
return Ok(pos as u16);
|
||||
} else {
|
||||
pos += 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(64);
|
||||
}
|
||||
|
||||
/// Peek `look_ahead` bits ahead without discarding them from the buffer
|
||||
#[inline(always)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const fn peek_bits<const LOOKAHEAD: u8>(&self) -> i32 {
|
||||
(self.aligned_buffer >> (64 - LOOKAHEAD)) as i32
|
||||
}
|
||||
|
||||
/// Discard the next `N` bits without checking
|
||||
#[inline]
|
||||
fn drop_bits(&mut self, n: u8) {
|
||||
// PS: Its a good check, but triggers fuzzer and a lot of false positives
|
||||
//debug_assert!(self.bits_left >= n);
|
||||
//self.bits_left -= n;
|
||||
self.bits_left = self.bits_left.saturating_sub(n);
|
||||
self.aligned_buffer <<= n;
|
||||
}
|
||||
|
||||
/// Read `n_bits` from the buffer and discard them
|
||||
#[inline(always)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn get_bits(&mut self, n_bits: u8) -> i32 {
|
||||
let mask = (1_u64 << n_bits) - 1;
|
||||
|
||||
self.aligned_buffer = self.aligned_buffer.rotate_left(u32::from(n_bits));
|
||||
let bits = (self.aligned_buffer & mask) as i32;
|
||||
self.bits_left = self.bits_left.wrapping_sub(n_bits);
|
||||
bits
|
||||
}
|
||||
|
||||
/// Decode a DC block
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[inline]
|
||||
pub(crate) fn decode_prog_dc_first<T>(
|
||||
&mut self, reader: &mut ZReader<T>, dc_table: &HuffmanTable, block: &mut i16,
|
||||
dc_prediction: &mut i32
|
||||
) -> Result<(), DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
self.decode_dc(reader, dc_table, dc_prediction)?;
|
||||
*block = (*dc_prediction as i16).wrapping_mul(self.successive_low_mask);
|
||||
return Ok(());
|
||||
}
|
||||
#[inline]
|
||||
pub(crate) fn decode_prog_dc_refine<T>(
|
||||
&mut self, reader: &mut ZReader<T>, block: &mut i16
|
||||
) -> Result<(), DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
// refinement scan
|
||||
if self.bits_left < 1 {
|
||||
self.refill(reader)?;
|
||||
// if we find a marker, it may happens we don't refill.
|
||||
// So let's confirm again that refill worked
|
||||
if self.bits_left < 1 {
|
||||
return Err(DecodeErrors::Format(
|
||||
"Marker found where not expected in refine bit".to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if self.get_bit() == 1 {
|
||||
*block = block.wrapping_add(self.successive_low_mask);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a single bit from the bitstream
|
||||
fn get_bit(&mut self) -> u8 {
|
||||
let k = (self.aligned_buffer >> 63) as u8;
|
||||
// discard a bit
|
||||
self.drop_bits(1);
|
||||
return k;
|
||||
}
|
||||
pub(crate) fn decode_mcu_ac_first<T>(
|
||||
&mut self, reader: &mut ZReader<T>, ac_table: &HuffmanTable, block: &mut [i16; 64]
|
||||
) -> Result<bool, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
let fast_ac = ac_table.ac_lookup.as_ref().unwrap();
|
||||
let bit = self.successive_low_mask;
|
||||
|
||||
let mut k = self.spec_start as usize;
|
||||
let (mut symbol, mut r, mut fac);
|
||||
|
||||
// EOB runs are handled in mcu_prog.rs
|
||||
'block: loop {
|
||||
self.refill(reader)?;
|
||||
// Check for marker in the stream
|
||||
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
fac = fast_ac[symbol as usize];
|
||||
symbol = ac_table.lookup[symbol as usize];
|
||||
|
||||
if fac != 0 {
|
||||
// fast ac path
|
||||
k += ((fac >> 4) & 15) as usize; // run
|
||||
block[UN_ZIGZAG[min(k, 63)] & 63] = (fac >> 8).wrapping_mul(bit); // value
|
||||
self.drop_bits((fac & 15) as u8);
|
||||
k += 1;
|
||||
} else {
|
||||
decode_huff!(self, symbol, ac_table);
|
||||
|
||||
r = symbol >> 4;
|
||||
symbol &= 15;
|
||||
|
||||
if symbol != 0 {
|
||||
k += r as usize;
|
||||
r = self.get_bits(symbol as u8);
|
||||
symbol = huff_extend(r, symbol);
|
||||
block[UN_ZIGZAG[k & 63] & 63] = (symbol as i16).wrapping_mul(bit);
|
||||
k += 1;
|
||||
} else {
|
||||
if r != 15 {
|
||||
self.eob_run = 1 << r;
|
||||
self.eob_run += self.get_bits(r as u8);
|
||||
self.eob_run -= 1;
|
||||
break;
|
||||
}
|
||||
|
||||
k += 16;
|
||||
}
|
||||
}
|
||||
|
||||
if k > self.spec_end as usize {
|
||||
break 'block;
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
#[allow(clippy::too_many_lines, clippy::op_ref)]
|
||||
pub(crate) fn decode_mcu_ac_refine<T>(
|
||||
&mut self, reader: &mut ZReader<T>, table: &HuffmanTable, block: &mut [i16; 64]
|
||||
) -> Result<bool, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
let bit = self.successive_low_mask;
|
||||
|
||||
let mut k = self.spec_start;
|
||||
let (mut symbol, mut r);
|
||||
|
||||
if self.eob_run == 0 {
|
||||
'no_eob: loop {
|
||||
// Decode a coefficient from the bit stream
|
||||
self.refill(reader)?;
|
||||
|
||||
symbol = self.peek_bits::<HUFF_LOOKAHEAD>();
|
||||
symbol = table.lookup[symbol as usize];
|
||||
|
||||
decode_huff!(self, symbol, table);
|
||||
|
||||
r = symbol >> 4;
|
||||
symbol &= 15;
|
||||
|
||||
if symbol == 0 {
|
||||
if r != 15 {
|
||||
// EOB run is 2^r + bits
|
||||
self.eob_run = 1 << r;
|
||||
self.eob_run += self.get_bits(r as u8);
|
||||
// EOB runs are handled by the eob logic
|
||||
break 'no_eob;
|
||||
}
|
||||
} else {
|
||||
if symbol != 1 {
|
||||
return Err(DecodeErrors::HuffmanDecode(
|
||||
"Bad Huffman code, corrupt JPEG?".to_string()
|
||||
));
|
||||
}
|
||||
// get sign bit
|
||||
// We assume we have enough bits, which should be correct for sane images
|
||||
// since we refill by 32 above
|
||||
if self.get_bit() == 1 {
|
||||
symbol = i32::from(bit);
|
||||
} else {
|
||||
symbol = i32::from(-bit);
|
||||
}
|
||||
}
|
||||
|
||||
// Advance over already nonzero coefficients appending
|
||||
// correction bits to the non-zeroes.
|
||||
// A correction bit is 1 if the absolute value of the coefficient must be increased
|
||||
|
||||
if k <= self.spec_end {
|
||||
'advance_nonzero: loop {
|
||||
let coefficient = &mut block[UN_ZIGZAG[k as usize & 63] & 63];
|
||||
|
||||
if *coefficient != 0 {
|
||||
if self.bits_left < 1 {
|
||||
self.refill(reader)?;
|
||||
if self.bits_left < 1 && self.marker.is_some() {
|
||||
return Err(DecodeErrors::Format(
|
||||
"Marker found where not expected in refine bit".to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.get_bit() == 1 && (*coefficient & bit) == 0 {
|
||||
if *coefficient > 0 {
|
||||
*coefficient += bit;
|
||||
} else {
|
||||
*coefficient -= bit;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r -= 1;
|
||||
|
||||
if r < 0 {
|
||||
// reached target zero coefficient.
|
||||
break 'advance_nonzero;
|
||||
}
|
||||
};
|
||||
|
||||
if k == self.spec_end {
|
||||
break 'advance_nonzero;
|
||||
}
|
||||
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if symbol != 0 {
|
||||
let pos = UN_ZIGZAG[k as usize & 63];
|
||||
// output new non-zero coefficient.
|
||||
block[pos & 63] = symbol as i16;
|
||||
}
|
||||
|
||||
k += 1;
|
||||
|
||||
if k > self.spec_end {
|
||||
break 'no_eob;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.eob_run > 0 {
|
||||
// only run if block does not consists of purely zeroes
|
||||
if &block[1..] != &[0; 63] {
|
||||
self.refill(reader)?;
|
||||
|
||||
while k <= self.spec_end {
|
||||
let coefficient = &mut block[UN_ZIGZAG[k as usize & 63] & 63];
|
||||
|
||||
if *coefficient != 0 && self.get_bit() == 1 {
|
||||
// check if we already modified it, if so do nothing, otherwise
|
||||
// append the correction bit.
|
||||
if (*coefficient & bit) == 0 {
|
||||
if *coefficient >= 0 {
|
||||
*coefficient = coefficient.wrapping_add(bit);
|
||||
} else {
|
||||
*coefficient = coefficient.wrapping_sub(bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.bits_left < 1 {
|
||||
// refill at the last possible moment
|
||||
self.refill(reader)?;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
// count a block completed in EOB run
|
||||
self.eob_run -= 1;
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub fn update_progressive_params(&mut self, _ah: u8, al: u8, spec_start: u8, spec_end: u8) {
|
||||
self.successive_low_mask = 1i16 << al;
|
||||
self.spec_start = spec_start;
|
||||
self.spec_end = spec_end;
|
||||
}
|
||||
|
||||
/// Reset the stream if we have a restart marker
|
||||
///
|
||||
/// Restart markers indicate drop those bits in the stream and zero out
|
||||
/// everything
|
||||
#[cold]
|
||||
pub fn reset(&mut self) {
|
||||
self.bits_left = 0;
|
||||
self.marker = None;
|
||||
self.buffer = 0;
|
||||
self.aligned_buffer = 0;
|
||||
self.eob_run = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Do the equivalent of JPEG HUFF_EXTEND
|
||||
#[inline(always)]
|
||||
fn huff_extend(x: i32, s: i32) -> i32 {
|
||||
// if x<s return x else return x+offset[s] where offset[s] = ( (-1<<s)+1)
|
||||
(x) + ((((x) - (1 << ((s) - 1))) >> 31) & (((-1) << (s)) + 1))
|
||||
}
|
||||
|
||||
const fn has_zero(v: u32) -> bool {
|
||||
// Retrieved from Stanford bithacks
|
||||
// @ https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord
|
||||
return !((((v & 0x7F7F_7F7F) + 0x7F7F_7F7F) | v) | 0x7F7F_7F7F) != 0;
|
||||
}
|
||||
|
||||
const fn has_byte(b: u32, val: u8) -> bool {
|
||||
// Retrieved from Stanford bithacks
|
||||
// @ https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord
|
||||
has_zero(b ^ ((!0_u32 / 255) * (val as u32)))
|
||||
}
|
||||
|
||||
// mod tests {
|
||||
// use zune_core::bytestream::ZCursor;
|
||||
// use zune_core::colorspace::ColorSpace;
|
||||
// use zune_core::options::DecoderOptions;
|
||||
//
|
||||
// use crate::JpegDecoder;
|
||||
//
|
||||
// #[test]
|
||||
// fn test_image() {
|
||||
// let img = "/Users/etemesi/Downloads/test_IDX_45_RAND_168601280367171438891916_minimized_837.jpg";
|
||||
// let data = std::fs::read(img).unwrap();
|
||||
// let options = DecoderOptions::new_cmd().jpeg_set_out_colorspace(ColorSpace::RGB);
|
||||
// let mut decoder = JpegDecoder::new_with_options(ZCursor::new(&data[..]), options);
|
||||
//
|
||||
// decoder.decode().unwrap();
|
||||
// println!("{:?}", decoder.options.jpeg_get_out_colorspace())
|
||||
// }
|
||||
// }
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![allow(
|
||||
clippy::many_single_char_names,
|
||||
clippy::similar_names,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::too_many_arguments,
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
//! Color space conversion routines
|
||||
//!
|
||||
//! This files exposes functions to convert one colorspace to another in a jpeg
|
||||
//! image
|
||||
//!
|
||||
//! Currently supported conversions are
|
||||
//!
|
||||
//! - `YCbCr` to `RGB,RGBA,GRAYSCALE,RGBX`.
|
||||
//!
|
||||
//!
|
||||
//! Hey there, if your reading this it means you probably need something, so let me help you.
|
||||
//!
|
||||
//! There are 3 supported cpu extensions here.
|
||||
//! 1. Scalar
|
||||
//! 2. SSE
|
||||
//! 3. AVX
|
||||
//!
|
||||
//! There are two types of the color convert functions
|
||||
//!
|
||||
//! 1. Acts on 16 pixels.
|
||||
//! 2. Acts on 8 pixels.
|
||||
//!
|
||||
//! The reason for this is because when implementing the AVX part it occurred to me that we can actually
|
||||
//! do better and process 2 MCU's if we change IDCT return type to be `i16's`, since a lot of
|
||||
//! CPU's these days support AVX extensions, it becomes nice if we optimize for that path ,
|
||||
//! therefore AVX routines can process 16 pixels directly and SSE and Scalar just compensate.
|
||||
//!
|
||||
//! By compensating, I mean I wrote the 16 pixels version operating on the 8 pixel version twice.
|
||||
//!
|
||||
//! Therefore if your looking to optimize some routines, probably start there.
|
||||
|
||||
pub use scalar::ycbcr_to_grayscale;
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::options::DecoderOptions;
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
pub use crate::color_convert::avx::{ycbcr_to_rgb_avx2, ycbcr_to_rgba_avx2};
|
||||
use crate::decoder::ColorConvert16Ptr;
|
||||
|
||||
mod avx;
|
||||
mod neon64;
|
||||
mod scalar;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn choose_ycbcr_to_rgb_convert_func(
|
||||
type_need: ColorSpace, options: &DecoderOptions
|
||||
) -> Option<ColorConvert16Ptr> {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
{
|
||||
use zune_core::log::debug;
|
||||
if options.use_avx2() {
|
||||
debug!("Using AVX optimised color conversion functions");
|
||||
|
||||
// I believe avx2 means sse4 is also available
|
||||
// match colorspace
|
||||
match type_need {
|
||||
ColorSpace::RGB => return Some(ycbcr_to_rgb_avx2),
|
||||
ColorSpace::RGBA => return Some(ycbcr_to_rgba_avx2),
|
||||
_ => () // fall through to scalar, which has more types
|
||||
};
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "neon", target_arch = "aarch64"))]
|
||||
{
|
||||
if options.use_neon() {
|
||||
use crate::color_convert::neon64::{ycbcr_to_rgb_neon, ycbcr_to_rgba_neon};
|
||||
match type_need {
|
||||
ColorSpace::RGB => return Some(ycbcr_to_rgb_neon),
|
||||
ColorSpace::RGBA => return Some(ycbcr_to_rgba_neon),
|
||||
_ => () // fall through to scalar, which has more types
|
||||
};
|
||||
}
|
||||
}
|
||||
// when there is no x86 or we haven't returned by here, resort to scalar
|
||||
return match type_need {
|
||||
ColorSpace::RGB => Some(scalar::ycbcr_to_rgb_inner_16_scalar::<false>),
|
||||
ColorSpace::RGBA => Some(scalar::ycbcr_to_rgba_inner_16_scalar::<false>),
|
||||
ColorSpace::BGRA => Some(scalar::ycbcr_to_rgba_inner_16_scalar::<true>),
|
||||
ColorSpace::BGR => Some(scalar::ycbcr_to_rgb_inner_16_scalar::<true>),
|
||||
_ => None
|
||||
};
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! AVX color conversion routines
|
||||
//!
|
||||
//! Okay these codes are cool
|
||||
//!
|
||||
//! Herein lies super optimized codes to do color conversions.
|
||||
//!
|
||||
//!
|
||||
//! 1. The YCbCr to RGB use integer approximations and not the floating point equivalent.
|
||||
//! That means we may be +- 2 of pixels generated by libjpeg-turbo jpeg decoding
|
||||
//! (also libjpeg uses routines like `Y = 0.29900 * R + 0.33700 * G + 0.11400 * B + 0.25000 * G`)
|
||||
//!
|
||||
//! Firstly, we use integers (fun fact:there is no part of this code base where were dealing with
|
||||
//! floating points.., fun fact: the first fun fact wasn't even fun.)
|
||||
//!
|
||||
//! Secondly ,we have cool clamping code, especially for rgba , where we don't need clamping and we
|
||||
//! spend our time cursing that Intel decided permute instructions to work like 2 128 bit vectors(the compiler opitmizes
|
||||
//! it out to something cool).
|
||||
//!
|
||||
//! There isn't a lot here (not as fun as bitstream ) but I hope you find what you're looking for.
|
||||
//!
|
||||
//! O and ~~subscribe to my youtube channel~~
|
||||
|
||||
#![cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#![cfg(feature = "x86")]
|
||||
#![allow(
|
||||
clippy::wildcard_imports,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::too_many_arguments,
|
||||
clippy::inline_always,
|
||||
clippy::doc_markdown,
|
||||
dead_code
|
||||
)]
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
use core::arch::x86::*;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use core::arch::x86_64::*;
|
||||
|
||||
use crate::color_convert::scalar::{CB_CF, CR_CF, C_G_CB_COEF_2, C_G_CR_COEF_1, YUV_RND, Y_CF};
|
||||
|
||||
pub union YmmRegister {
|
||||
// both are 32 when using std::mem::size_of
|
||||
mm256: __m256i,
|
||||
// for avx color conversion
|
||||
array: [i16; 16]
|
||||
}
|
||||
|
||||
const R_AVX_COEF: i32 = i32::from_ne_bytes([CR_CF.to_ne_bytes()[0], CR_CF.to_ne_bytes()[1], 0, 0]);
|
||||
const B_AVX_COEF: i32 = i32::from_ne_bytes([0, 0, CB_CF.to_ne_bytes()[0], CB_CF.to_ne_bytes()[1]]);
|
||||
const G_COEF_AVX_COEF: i32 = i32::from_ne_bytes([
|
||||
C_G_CR_COEF_1.to_ne_bytes()[0],
|
||||
C_G_CR_COEF_1.to_ne_bytes()[1],
|
||||
C_G_CB_COEF_2.to_ne_bytes()[0],
|
||||
C_G_CB_COEF_2.to_ne_bytes()[1]
|
||||
]);
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
// AVX conversion routines
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
///
|
||||
/// Convert YCBCR to RGB using AVX instructions
|
||||
///
|
||||
/// # Note
|
||||
///**IT IS THE RESPONSIBILITY OF THE CALLER TO CALL THIS IN CPUS SUPPORTING
|
||||
/// AVX2 OTHERWISE THIS IS UB**
|
||||
///
|
||||
/// *Peace*
|
||||
///
|
||||
/// This library itself will ensure that it's never called in CPU's not
|
||||
/// supporting AVX2
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `y`,`cb`,`cr`: A reference of 8 i32's
|
||||
/// - `out`: The output array where we store our converted items
|
||||
/// - `offset`: The position from 0 where we write these RGB values
|
||||
#[inline(always)]
|
||||
pub fn ycbcr_to_rgb_avx2(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], out: &mut [u8], offset: &mut usize
|
||||
) {
|
||||
// call this in another function to tell RUST to vectorize this
|
||||
// storing
|
||||
unsafe {
|
||||
ycbcr_to_rgb_avx2_1(y, cb, cr, out, offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn ycbcr_to_rgb_avx2_1(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], out: &mut [u8], offset: &mut usize
|
||||
) {
|
||||
let (mut r, mut g, mut b) = ycbcr_to_rgb_baseline_no_clamp(y, cb, cr);
|
||||
|
||||
r = _mm256_packus_epi16(r, _mm256_setzero_si256());
|
||||
g = _mm256_packus_epi16(g, _mm256_setzero_si256());
|
||||
b = _mm256_packus_epi16(b, _mm256_setzero_si256());
|
||||
|
||||
r = _mm256_permute4x64_epi64::<{ shuffle(3, 1, 2, 0) }>(r);
|
||||
g = _mm256_permute4x64_epi64::<{ shuffle(3, 1, 2, 0) }>(g);
|
||||
b = _mm256_permute4x64_epi64::<{ shuffle(3, 1, 2, 0) }>(b);
|
||||
|
||||
let sh_r = _mm256_setr_epi8(
|
||||
0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14,
|
||||
9, 4, 15, 10, 5
|
||||
);
|
||||
let sh_g = _mm256_setr_epi8(
|
||||
5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3,
|
||||
14, 9, 4, 15, 10
|
||||
);
|
||||
let sh_b = _mm256_setr_epi8(
|
||||
10, 5, 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5, 0, 11, 6, 1, 12, 7, 2, 13, 8,
|
||||
3, 14, 9, 4, 15
|
||||
);
|
||||
|
||||
let r0 = _mm256_shuffle_epi8(r, sh_r);
|
||||
let g0 = _mm256_shuffle_epi8(g, sh_g);
|
||||
let b0 = _mm256_shuffle_epi8(b, sh_b);
|
||||
|
||||
let m0 = _mm256_setr_epi8(
|
||||
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
|
||||
0, 0, -1, 0, 0
|
||||
);
|
||||
let m1 = _mm256_setr_epi8(
|
||||
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0,
|
||||
-1, 0, 0, -1, 0
|
||||
);
|
||||
|
||||
let p0 = _mm256_blendv_epi8(_mm256_blendv_epi8(r0, g0, m0), b0, m1);
|
||||
let p1 = _mm256_blendv_epi8(_mm256_blendv_epi8(g0, b0, m0), r0, m1);
|
||||
let p2 = _mm256_blendv_epi8(_mm256_blendv_epi8(b0, r0, m0), g0, m1);
|
||||
|
||||
let rgb0 = _mm256_permute2x128_si256::<32>(p0, p1);
|
||||
let rgb1 = _mm256_permute2x128_si256::<48>(p2, p0);
|
||||
|
||||
_mm256_storeu_si256(out.as_mut_ptr().cast(), rgb0);
|
||||
_mm_storeu_si128(out[32..].as_mut_ptr().cast(), _mm256_castsi256_si128(rgb1));
|
||||
|
||||
*offset += 48;
|
||||
}
|
||||
|
||||
// Enabled avx2 automatically enables avx.
|
||||
#[inline]
|
||||
#[target_feature(enable = "avx2")]
|
||||
/// A baseline implementation of YCbCr to RGB conversion which does not carry
|
||||
/// out clamping
|
||||
///
|
||||
/// This is used by the `ycbcr_to_rgba_avx` and `ycbcr_to_rgbx` conversion
|
||||
/// routines
|
||||
unsafe fn ycbcr_to_rgb_baseline_no_clamp(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16]
|
||||
) -> (__m256i, __m256i, __m256i) {
|
||||
// Load values into a register
|
||||
//
|
||||
let y_c = _mm256_loadu_si256(y.as_ptr().cast());
|
||||
let cb_c = _mm256_loadu_si256(cb.as_ptr().cast());
|
||||
let cr_c = _mm256_loadu_si256(cr.as_ptr().cast());
|
||||
|
||||
// Here we want to use _mm256_madd_epi16 to perform 2 multiplications
|
||||
// and one addition per instruction.
|
||||
|
||||
// At first, we have to pack i16 U and V that stores u8 into one u8 [U,V]
|
||||
// then zero extend, and keep in mind that lanes is already been permuted.
|
||||
|
||||
let y_coeff = _mm256_set1_epi32(i32::from(Y_CF));
|
||||
let cr_coeff = _mm256_set1_epi32(R_AVX_COEF);
|
||||
let cb_coeff = _mm256_set1_epi32(B_AVX_COEF);
|
||||
let cg_coeff = _mm256_set1_epi32(G_COEF_AVX_COEF);
|
||||
let v_rnd = _mm256_set1_epi32(i32::from(YUV_RND));
|
||||
let uv_bias = _mm256_set1_epi16(128);
|
||||
|
||||
// UV in memory because x86/x86_64 is always little endian
|
||||
let v_0 = _mm256_slli_epi16::<8>(cb_c);
|
||||
let u_v_8 = _mm256_or_si256(v_0, cr_c);
|
||||
|
||||
let mut u_v_lo = _mm256_unpacklo_epi8(u_v_8, _mm256_setzero_si256());
|
||||
let mut u_v_hi = _mm256_unpackhi_epi8(u_v_8, _mm256_setzero_si256());
|
||||
|
||||
let mut y_lo = _mm256_unpacklo_epi16(y_c, _mm256_setzero_si256());
|
||||
let mut y_hi = _mm256_unpackhi_epi16(y_c, _mm256_setzero_si256());
|
||||
|
||||
u_v_lo = _mm256_sub_epi16(u_v_lo, uv_bias);
|
||||
u_v_hi = _mm256_sub_epi16(u_v_hi, uv_bias);
|
||||
|
||||
y_lo = _mm256_madd_epi16(y_lo, y_coeff);
|
||||
y_hi = _mm256_madd_epi16(y_hi, y_coeff);
|
||||
|
||||
let mut r_lo = _mm256_madd_epi16(u_v_lo, cr_coeff);
|
||||
let mut r_hi = _mm256_madd_epi16(u_v_hi, cr_coeff);
|
||||
|
||||
let mut g_lo = _mm256_madd_epi16(u_v_lo, cg_coeff);
|
||||
let mut g_hi = _mm256_madd_epi16(u_v_hi, cg_coeff);
|
||||
|
||||
// This ordering is preferred to reduce register file pressure.
|
||||
|
||||
y_lo = _mm256_add_epi32(y_lo, v_rnd);
|
||||
y_hi = _mm256_add_epi32(y_hi, v_rnd);
|
||||
|
||||
let mut b_lo = _mm256_madd_epi16(u_v_lo, cb_coeff);
|
||||
let mut b_hi = _mm256_madd_epi16(u_v_hi, cb_coeff);
|
||||
|
||||
r_lo = _mm256_add_epi32(r_lo, y_lo);
|
||||
r_hi = _mm256_add_epi32(r_hi, y_hi);
|
||||
|
||||
g_lo = _mm256_add_epi32(g_lo, y_lo);
|
||||
g_hi = _mm256_add_epi32(g_hi, y_hi);
|
||||
|
||||
b_lo = _mm256_add_epi32(b_lo, y_lo);
|
||||
b_hi = _mm256_add_epi32(b_hi, y_hi);
|
||||
|
||||
r_lo = _mm256_srai_epi32::<14>(r_lo);
|
||||
r_hi = _mm256_srai_epi32::<14>(r_hi);
|
||||
|
||||
g_lo = _mm256_srai_epi32::<14>(g_lo);
|
||||
g_hi = _mm256_srai_epi32::<14>(g_hi);
|
||||
|
||||
b_lo = _mm256_srai_epi32::<14>(b_lo);
|
||||
b_hi = _mm256_srai_epi32::<14>(b_hi);
|
||||
|
||||
let r = _mm256_packus_epi32(r_lo, r_hi);
|
||||
let g = _mm256_packus_epi32(g_lo, g_hi);
|
||||
let b = _mm256_packus_epi32(b_lo, b_hi);
|
||||
|
||||
return (r, g, b);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ycbcr_to_rgba_avx2(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], out: &mut [u8], offset: &mut usize
|
||||
) {
|
||||
unsafe {
|
||||
ycbcr_to_rgba_unsafe(y, cb, cr, out, offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[rustfmt::skip]
|
||||
unsafe fn ycbcr_to_rgba_unsafe(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16],
|
||||
out: &mut [u8],
|
||||
offset: &mut usize,
|
||||
)
|
||||
{
|
||||
// check if we have enough space to write.
|
||||
let tmp:& mut [u8; 64] = out.get_mut(*offset..*offset + 64).expect("Slice to small cannot write").try_into().unwrap();
|
||||
|
||||
let (r, g, b) = ycbcr_to_rgb_baseline_no_clamp(y, cb, cr);
|
||||
|
||||
// set alpha channel to 255 for opaque
|
||||
|
||||
// And no these comments were not from me pressing the keyboard
|
||||
|
||||
// Pack the integers into u8's using unsigned saturation.
|
||||
let c = _mm256_packus_epi16(r, g); //aaaaa_bbbbb_aaaaa_bbbbbb
|
||||
let d = _mm256_packus_epi16(b, _mm256_set1_epi16(255)); // cccccc_dddddd_ccccccc_ddddd
|
||||
// transpose_u16 and interleave channels
|
||||
let e = _mm256_unpacklo_epi8(c, d); //ab_ab_ab_ab_ab_ab_ab_ab
|
||||
let f = _mm256_unpackhi_epi8(c, d); //cd_cd_cd_cd_cd_cd_cd_cd
|
||||
// final transpose_u16
|
||||
let g = _mm256_unpacklo_epi8(e, f); //abcd_abcd_abcd_abcd_abcd
|
||||
let h = _mm256_unpackhi_epi8(e, f);
|
||||
|
||||
// undo packus shuffling...
|
||||
let i = _mm256_permute2x128_si256::<{ shuffle(3, 2, 1, 0) }>(g, h);
|
||||
|
||||
let j = _mm256_permute2x128_si256::<{ shuffle(1, 2, 3, 0) }>(g, h);
|
||||
|
||||
let k = _mm256_permute2x128_si256::<{ shuffle(3, 2, 0, 1) }>(g, h);
|
||||
|
||||
let l = _mm256_permute2x128_si256::<{ shuffle(0, 3, 2, 1) }>(g, h);
|
||||
|
||||
let m = _mm256_blend_epi32::<0b1111_0000>(i, j);
|
||||
|
||||
let n = _mm256_blend_epi32::<0b1111_0000>(k, l);
|
||||
|
||||
// Store
|
||||
// Use streaming instructions to prevent polluting the cache?
|
||||
_mm256_storeu_si256(tmp.as_mut_ptr().cast(), m);
|
||||
|
||||
_mm256_storeu_si256(tmp[32..].as_mut_ptr().cast(), n);
|
||||
|
||||
*offset += 64;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn shuffle(z: i32, y: i32, x: i32, w: i32) -> i32 {
|
||||
(z << 6) | (y << 4) | (x << 2) | w
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (c) 2025.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Aarch64 color conversion routines
|
||||
//! NEON is mandatory on aarch64.
|
||||
|
||||
#![cfg(all(feature = "neon", target_arch = "aarch64"))]
|
||||
use core::arch::aarch64::*;
|
||||
|
||||
use crate::color_convert::scalar::{CB_CF, CR_CF, C_G_CB_COEF_2, C_G_CR_COEF_1, YUV_RND, Y_CF};
|
||||
|
||||
const C_1: u64 = u64::from_ne_bytes([
|
||||
Y_CF.to_ne_bytes()[0],
|
||||
Y_CF.to_ne_bytes()[1],
|
||||
CR_CF.to_ne_bytes()[0],
|
||||
CR_CF.to_ne_bytes()[1],
|
||||
CB_CF.to_ne_bytes()[0],
|
||||
CB_CF.to_ne_bytes()[1],
|
||||
C_G_CR_COEF_1.to_ne_bytes()[0],
|
||||
C_G_CR_COEF_1.to_ne_bytes()[1]
|
||||
]);
|
||||
const C_2: u64 = u64::from_ne_bytes([
|
||||
C_G_CB_COEF_2.to_ne_bytes()[0],
|
||||
C_G_CB_COEF_2.to_ne_bytes()[1],
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]);
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn ycbcr_to_rgb_baseline_no_clamp(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16]
|
||||
) -> (uint8x16_t, uint8x16_t, uint8x16_t) {
|
||||
// NEON has 32 registers, so it is good idea to utilize a lot of variables at once
|
||||
|
||||
let cb_cr_bias = vdupq_n_s16(128);
|
||||
// 0 - Y coeff, 1 - Cr, 2 - Cb, 3 - G1, 4 - G2
|
||||
let coefficients = vcombine_s16(vcreate_s16(C_1), vcreate_s16(C_2));
|
||||
|
||||
let y0 = vld1q_s16(y.as_ptr().cast());
|
||||
let y1 = vld1q_s16(y[8..].as_ptr().cast());
|
||||
|
||||
let mut cb0 = vld1q_s16(cb.as_ptr().cast());
|
||||
let mut cb1 = vld1q_s16(cb[8..].as_ptr().cast());
|
||||
|
||||
let mut cr0 = vld1q_s16(cr.as_ptr().cast());
|
||||
let mut cr1 = vld1q_s16(cr[8..].as_ptr().cast());
|
||||
|
||||
cb0 = vsubq_s16(cb0, cb_cr_bias);
|
||||
cb1 = vsubq_s16(cb1, cb_cr_bias);
|
||||
|
||||
cr0 = vsubq_s16(cr0, cb_cr_bias);
|
||||
cr1 = vsubq_s16(cr1, cb_cr_bias);
|
||||
|
||||
let bias = vdupq_n_s32(i32::from(YUV_RND));
|
||||
|
||||
let acc0 = vmlal_laneq_s16::<0>(bias, vget_low_s16(y0), coefficients);
|
||||
let acc1 = vmlal_high_laneq_s16::<0>(bias, y0, coefficients);
|
||||
let acc2 = vmlal_laneq_s16::<0>(bias, vget_low_s16(y1), coefficients);
|
||||
let acc3 = vmlal_high_laneq_s16::<0>(bias, y1, coefficients);
|
||||
|
||||
let r0 = vmlal_laneq_s16::<1>(acc0, vget_low_s16(cr0), coefficients);
|
||||
let r1 = vmlal_high_laneq_s16::<1>(acc1, cr0, coefficients);
|
||||
let r2 = vmlal_laneq_s16::<1>(acc2, vget_low_s16(cr1), coefficients);
|
||||
let r3 = vmlal_high_laneq_s16::<1>(acc3, cr1, coefficients);
|
||||
|
||||
let b0 = vmlal_laneq_s16::<2>(acc0, vget_low_s16(cb0), coefficients);
|
||||
let b1 = vmlal_high_laneq_s16::<2>(acc1, cb0, coefficients);
|
||||
let b2 = vmlal_laneq_s16::<2>(acc2, vget_low_s16(cb1), coefficients);
|
||||
let b3 = vmlal_high_laneq_s16::<2>(acc3, cb1, coefficients);
|
||||
|
||||
// Saturating shift right with signed -> unsigned saturation
|
||||
let qr0 = vqshrun_n_s32::<14>(r0);
|
||||
let qr1 = vqshrun_n_s32::<14>(r1);
|
||||
let qr2 = vqshrun_n_s32::<14>(r2);
|
||||
let qr3 = vqshrun_n_s32::<14>(r3);
|
||||
|
||||
let mut g0 = vmlal_laneq_s16::<4>(acc0, vget_low_s16(cb0), coefficients);
|
||||
let mut g1 = vmlal_high_laneq_s16::<4>(acc1, cb0, coefficients);
|
||||
let mut g2 = vmlal_laneq_s16::<4>(acc2, vget_low_s16(cb1), coefficients);
|
||||
let mut g3 = vmlal_high_laneq_s16::<4>(acc3, cb1, coefficients);
|
||||
|
||||
let qb0 = vqshrun_n_s32::<14>(b0);
|
||||
let qb1 = vqshrun_n_s32::<14>(b1);
|
||||
let qb2 = vqshrun_n_s32::<14>(b2);
|
||||
let qb3 = vqshrun_n_s32::<14>(b3);
|
||||
|
||||
let r0 = vqmovn_u16(vcombine_u16(qr0, qr1));
|
||||
let r1 = vqmovn_u16(vcombine_u16(qr2, qr3));
|
||||
|
||||
let b0 = vqmovn_u16(vcombine_u16(qb0, qb1));
|
||||
let b1 = vqmovn_u16(vcombine_u16(qb2, qb3));
|
||||
|
||||
g0 = vmlal_laneq_s16::<3>(g0, vget_low_s16(cr0), coefficients);
|
||||
g1 = vmlal_high_laneq_s16::<3>(g1, cr0, coefficients);
|
||||
g2 = vmlal_laneq_s16::<3>(g2, vget_low_s16(cr1), coefficients);
|
||||
g3 = vmlal_high_laneq_s16::<3>(g3, cr1, coefficients);
|
||||
|
||||
let qg0 = vqshrun_n_s32::<14>(g0);
|
||||
let qg1 = vqshrun_n_s32::<14>(g1);
|
||||
let qg2 = vqshrun_n_s32::<14>(g2);
|
||||
let qg3 = vqshrun_n_s32::<14>(g3);
|
||||
|
||||
let g0 = vqmovn_u16(vcombine_u16(qg0, qg1));
|
||||
let g1 = vqmovn_u16(vcombine_u16(qg2, qg3));
|
||||
|
||||
(
|
||||
vcombine_u8(r0, r1),
|
||||
vcombine_u8(g0, g1),
|
||||
vcombine_u8(b0, b1)
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ycbcr_to_rgb_neon(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], out: &mut [u8], offset: &mut usize
|
||||
) {
|
||||
// call this in another function to tell RUST to vectorize this
|
||||
// storing
|
||||
unsafe {
|
||||
let (r, g, b) = ycbcr_to_rgb_baseline_no_clamp(y, cb, cr);
|
||||
vst3q_u8(out.as_mut_ptr(), uint8x16x3_t(r, g, b));
|
||||
*offset += 48;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ycbcr_to_rgba_neon(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], out: &mut [u8], offset: &mut usize
|
||||
) {
|
||||
unsafe {
|
||||
let (r, g, b) = ycbcr_to_rgb_baseline_no_clamp(y, cb, cr);
|
||||
vst4q_u8(out.as_mut_ptr(), uint8x16x4_t(r, g, b, vdupq_n_u8(255)));
|
||||
*offset += 64;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
use core::convert::TryInto;
|
||||
|
||||
// Bt.601 Full Range inverse coefficients computed with 14 bits of precision with MPFR.
|
||||
// This is important to keep them in i16.
|
||||
// In most cases LLVM will detect what we're doing i16 widening to i32 math and will use
|
||||
// appropriate optimizations.
|
||||
pub(crate) const Y_CF: i16 = 16384;
|
||||
pub(crate) const CR_CF: i16 = 22970;
|
||||
pub(crate) const CB_CF: i16 = 29032;
|
||||
pub(crate) const C_G_CR_COEF_1: i16 = -11700;
|
||||
pub(crate) const C_G_CB_COEF_2: i16 = -5638;
|
||||
pub(crate) const YUV_PREC: i16 = 14;
|
||||
// Rounding const for YUV -> RGB conversion: floating equivalent 0.499(9).
|
||||
pub(crate) const YUV_RND: i16 = (1 << (YUV_PREC - 1)) - 1;
|
||||
|
||||
/// Limit values to 0 and 255
|
||||
#[inline]
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, dead_code)]
|
||||
fn clamp(a: i32) -> u8 {
|
||||
a.clamp(0, 255) as u8
|
||||
}
|
||||
|
||||
/// YCbCr to RGBA color conversion
|
||||
|
||||
/// Convert YCbCr to RGB/BGR
|
||||
///
|
||||
/// Converts to RGB if const BGRA is false
|
||||
///
|
||||
/// Converts to BGR if const BGRA is true
|
||||
pub fn ycbcr_to_rgba_inner_16_scalar<const BGRA: bool>(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], output: &mut [u8], pos: &mut usize
|
||||
) {
|
||||
let (_, output_position) = output.split_at_mut(*pos);
|
||||
|
||||
// Convert into a slice with 64 elements for Rust to see we won't go out of bounds.
|
||||
let opt: &mut [u8; 64] = output_position
|
||||
.get_mut(0..64)
|
||||
.expect("Slice to small cannot write")
|
||||
.try_into()
|
||||
.unwrap();
|
||||
for ((&y, (cb, cr)), out) in y
|
||||
.iter()
|
||||
.zip(cb.iter().zip(cr.iter()))
|
||||
.zip(opt.chunks_exact_mut(4))
|
||||
{
|
||||
let cr = cr - 128;
|
||||
let cb = cb - 128;
|
||||
|
||||
let y0 = i32::from(y) * i32::from(Y_CF) + i32::from(YUV_RND);
|
||||
|
||||
let r = (y0 + i32::from(cr) * i32::from(CR_CF)) >> YUV_PREC;
|
||||
let g = (y0
|
||||
+ i32::from(cr) * i32::from(C_G_CR_COEF_1)
|
||||
+ i32::from(cb) * i32::from(C_G_CB_COEF_2))
|
||||
>> YUV_PREC;
|
||||
let b = (y0 + i32::from(cb) * i32::from(CB_CF)) >> YUV_PREC;
|
||||
|
||||
if BGRA {
|
||||
out[0] = clamp(b);
|
||||
out[1] = clamp(g);
|
||||
out[2] = clamp(r);
|
||||
out[3] = 255;
|
||||
} else {
|
||||
out[0] = clamp(r);
|
||||
out[1] = clamp(g);
|
||||
out[2] = clamp(b);
|
||||
out[3] = 255;
|
||||
}
|
||||
}
|
||||
*pos += 64;
|
||||
}
|
||||
|
||||
/// Convert YCbCr to RGB/BGR
|
||||
///
|
||||
/// Converts to RGB if const BGRA is false
|
||||
///
|
||||
/// Converts to BGR if const BGRA is true
|
||||
pub fn ycbcr_to_rgb_inner_16_scalar<const BGRA: bool>(
|
||||
y: &[i16; 16], cb: &[i16; 16], cr: &[i16; 16], output: &mut [u8], pos: &mut usize
|
||||
) {
|
||||
let (_, output_position) = output.split_at_mut(*pos);
|
||||
|
||||
// Convert into a slice with 48 elements
|
||||
let opt: &mut [u8; 48] = output_position
|
||||
.get_mut(0..48)
|
||||
.expect("Slice to small cannot write")
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
for ((&y, (cb, cr)), out) in y
|
||||
.iter()
|
||||
.zip(cb.iter().zip(cr.iter()))
|
||||
.zip(opt.chunks_exact_mut(3))
|
||||
{
|
||||
let cr = cr - 128;
|
||||
let cb = cb - 128;
|
||||
|
||||
let y0 = i32::from(y) * i32::from(Y_CF) + i32::from(YUV_RND);
|
||||
|
||||
let r = (y0 + i32::from(cr) * i32::from(CR_CF)) >> YUV_PREC;
|
||||
let g = (y0
|
||||
+ i32::from(cr) * i32::from(C_G_CR_COEF_1)
|
||||
+ i32::from(cb) * i32::from(C_G_CB_COEF_2))
|
||||
>> YUV_PREC;
|
||||
let b = (y0 + i32::from(cb) * i32::from(CB_CF)) >> YUV_PREC;
|
||||
|
||||
if BGRA {
|
||||
out[0] = clamp(b);
|
||||
out[1] = clamp(g);
|
||||
out[2] = clamp(r);
|
||||
} else {
|
||||
out[0] = clamp(r);
|
||||
out[1] = clamp(g);
|
||||
out[2] = clamp(b);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment pos
|
||||
*pos += 48;
|
||||
}
|
||||
|
||||
pub fn ycbcr_to_grayscale(y: &[i16], width: usize, padded_width: usize, output: &mut [u8]) {
|
||||
for (y_in, out) in y
|
||||
.chunks_exact(padded_width)
|
||||
.zip(output.chunks_exact_mut(width))
|
||||
{
|
||||
for (y, out) in y_in.iter().zip(out.iter_mut()) {
|
||||
*out = *y as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! This module exports a single struct to store information about
|
||||
//! JPEG image components
|
||||
//!
|
||||
//! The data is extracted from a SOF header.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{format, vec};
|
||||
|
||||
use zune_core::log::trace;
|
||||
|
||||
use crate::alloc::string::ToString;
|
||||
use crate::decoder::MAX_COMPONENTS;
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::upsampler::upsample_no_op;
|
||||
const MAX_SAMP_FACTOR: usize = 4;
|
||||
|
||||
/// Represents an up-sampler function, this function will be called to upsample
|
||||
/// a down-sampled image
|
||||
|
||||
pub type UpSampler = fn(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch_space: &mut [i16],
|
||||
output: &mut [i16]
|
||||
);
|
||||
|
||||
/// Component Data from start of frame
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Components {
|
||||
/// The type of component that has the metadata below, can be Y,Cb or Cr
|
||||
pub component_id: ComponentID,
|
||||
/// Sub-sampling ratio of this component in the x-plane
|
||||
pub vertical_sample: usize,
|
||||
/// Sub-sampling ratio of this component in the y-plane
|
||||
pub horizontal_sample: usize,
|
||||
/// DC huffman table position
|
||||
pub dc_huff_table: usize,
|
||||
/// AC huffman table position for this element.
|
||||
pub ac_huff_table: usize,
|
||||
/// Quantization table number
|
||||
pub quantization_table_number: u8,
|
||||
/// Specifies quantization table to use with this component
|
||||
pub quantization_table: [i32; 64],
|
||||
/// dc prediction for the component
|
||||
pub dc_pred: i32,
|
||||
/// An up-sampling function, can be basic or SSE, depending
|
||||
/// on the platform
|
||||
pub up_sampler: UpSampler,
|
||||
/// How pixels do we need to go to get to the next line?
|
||||
pub width_stride: usize,
|
||||
/// Component ID for progressive
|
||||
pub id: u8,
|
||||
/// Whether we need to decode this image component.
|
||||
pub needed: bool,
|
||||
/// Upsample scanline
|
||||
pub raw_coeff: Vec<i16>,
|
||||
/// Upsample destination, stores a scanline worth of sub sampled data
|
||||
pub upsample_dest: Vec<i16>,
|
||||
/// previous row, used to handle MCU boundaries
|
||||
pub row_up: Vec<i16>,
|
||||
/// current row, used to handle MCU boundaries again
|
||||
pub row: Vec<i16>,
|
||||
pub first_row_upsample_dest: Vec<i16>,
|
||||
pub idct_pos: usize,
|
||||
pub x: usize,
|
||||
pub w2: usize,
|
||||
pub y: usize,
|
||||
pub sample_ratio: SampleRatios,
|
||||
// a very annoying bug
|
||||
pub fix_an_annoying_bug: usize
|
||||
}
|
||||
|
||||
impl Components {
|
||||
/// Create a new instance from three bytes from the start of frame
|
||||
#[inline]
|
||||
pub fn from(a: [u8; 3], pos: u8) -> Result<Components, DecodeErrors> {
|
||||
// it's a unique identifier.
|
||||
// doesn't have to be ascending
|
||||
// see tests/inputs/huge_sof_number
|
||||
//
|
||||
// For such cases, use the position of the component
|
||||
// to determine width
|
||||
|
||||
let id = match pos {
|
||||
0 => ComponentID::Y,
|
||||
1 => ComponentID::Cb,
|
||||
2 => ComponentID::Cr,
|
||||
3 => ComponentID::Q,
|
||||
_ => {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Unknown component id found,{pos}, expected value between 1 and 4"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let horizontal_sample = (a[1] >> 4) as usize;
|
||||
let vertical_sample = (a[1] & 0x0f) as usize;
|
||||
// Match libjpeg turbo on checking for sampling factors
|
||||
// Reject anything above 4
|
||||
if horizontal_sample > MAX_SAMP_FACTOR {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Bogus Horizontal Sampling Factor {horizontal_sample}"
|
||||
)));
|
||||
}
|
||||
if vertical_sample > MAX_SAMP_FACTOR {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Bogus Vertical Sampling Factor {vertical_sample}"
|
||||
)));
|
||||
}
|
||||
|
||||
let quantization_table_number = a[2];
|
||||
// confirm quantization number is between 0 and MAX_COMPONENTS
|
||||
if usize::from(quantization_table_number) >= MAX_COMPONENTS {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Too large quantization number :{quantization_table_number}, expected value between 0 and {MAX_COMPONENTS}"
|
||||
)));
|
||||
}
|
||||
// check that upsampling ratios are powers of two
|
||||
// if these fail, it's probably a corrupt image.
|
||||
if !horizontal_sample.is_power_of_two() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Horizontal sample is not a power of two({horizontal_sample}) cannot decode"
|
||||
)));
|
||||
}
|
||||
|
||||
// if !vertical_sample.is_power_of_two() {
|
||||
// return Err(DecodeErrors::Format(format!(
|
||||
// "Vertical sub-sample is not power of two({vertical_sample}) cannot decode"
|
||||
// )));
|
||||
// }
|
||||
if vertical_sample == 0 {
|
||||
// Check for invalid vertical sample
|
||||
return Err(DecodeErrors::Format("Vertical sample is zero".to_string()));
|
||||
}
|
||||
trace!(
|
||||
"Component ID:{:?} \tHS:{} VS:{} QT:{}",
|
||||
id,
|
||||
horizontal_sample,
|
||||
vertical_sample,
|
||||
quantization_table_number
|
||||
);
|
||||
|
||||
Ok(Components {
|
||||
component_id: id,
|
||||
vertical_sample,
|
||||
horizontal_sample,
|
||||
quantization_table_number,
|
||||
first_row_upsample_dest: vec![],
|
||||
// These two will be set with sof marker
|
||||
dc_huff_table: 0,
|
||||
ac_huff_table: 0,
|
||||
quantization_table: [0; 64],
|
||||
dc_pred: 0,
|
||||
up_sampler: upsample_no_op,
|
||||
// set later
|
||||
width_stride: horizontal_sample,
|
||||
id: a[0],
|
||||
needed: true,
|
||||
raw_coeff: vec![],
|
||||
upsample_dest: vec![],
|
||||
row_up: vec![],
|
||||
row: vec![],
|
||||
idct_pos: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w2: 0,
|
||||
sample_ratio: SampleRatios::None,
|
||||
fix_an_annoying_bug: 1
|
||||
})
|
||||
}
|
||||
/// Setup space for upsampling
|
||||
///
|
||||
/// During upsample, we need a reference of the last row so that upsampling can
|
||||
/// proceed correctly,
|
||||
/// so we store the last line of every scanline and use it for the next upsampling procedure
|
||||
/// to store this, but since we don't need it for 1v1 upsampling,
|
||||
/// we only call this for routines that need upsampling
|
||||
///
|
||||
/// # Requirements
|
||||
/// - width stride of this element is set for the component.
|
||||
pub fn setup_upsample_scanline(&mut self) {
|
||||
self.row = vec![0; self.width_stride * self.vertical_sample];
|
||||
self.row_up = vec![0; self.width_stride * self.vertical_sample];
|
||||
self.first_row_upsample_dest =
|
||||
vec![128; self.vertical_sample * self.width_stride * self.sample_ratio.sample()];
|
||||
self.upsample_dest =
|
||||
vec![0; self.width_stride * self.sample_ratio.sample() * self.fix_an_annoying_bug * 8];
|
||||
}
|
||||
}
|
||||
|
||||
/// Component ID's
|
||||
#[derive(Copy, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ComponentID {
|
||||
/// Luminance channel
|
||||
Y,
|
||||
/// Blue chrominance
|
||||
Cb,
|
||||
/// Red chrominance
|
||||
Cr,
|
||||
/// Q or fourth component
|
||||
Q
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum SampleRatios {
|
||||
HV,
|
||||
V,
|
||||
H,
|
||||
Generic(usize, usize),
|
||||
#[default]
|
||||
None
|
||||
}
|
||||
|
||||
impl SampleRatios {
|
||||
pub fn sample(self) -> usize {
|
||||
match self {
|
||||
SampleRatios::HV => 4,
|
||||
SampleRatios::V | SampleRatios::H => 2,
|
||||
SampleRatios::Generic(a, b) => a * b,
|
||||
SampleRatios::None => 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+987
@@ -0,0 +1,987 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Main image logic.
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{format, vec};
|
||||
|
||||
use zune_core::bytestream::{ZByteReaderTrait, ZReader};
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::log::{error, trace, warn};
|
||||
use zune_core::options::DecoderOptions;
|
||||
|
||||
use crate::color_convert::choose_ycbcr_to_rgb_convert_func;
|
||||
use crate::components::{Components, SampleRatios};
|
||||
use crate::errors::{DecodeErrors, UnsupportedSchemes};
|
||||
use crate::headers::{
|
||||
parse_app1, parse_app13, parse_app14, parse_app2, parse_dqt, parse_huffman, parse_sos,
|
||||
parse_start_of_frame
|
||||
};
|
||||
use crate::huffman::HuffmanTable;
|
||||
use crate::idct::{choose_idct_func, choose_idct_1x1_func, choose_idct_4x4_func};
|
||||
use crate::marker::Marker;
|
||||
use crate::misc::SOFMarkers;
|
||||
use crate::upsampler::{
|
||||
choose_horizontal_samp_function, choose_hv_samp_function, choose_v_samp_function,
|
||||
generic_sampler, upsample_no_op
|
||||
};
|
||||
|
||||
/// Maximum components
|
||||
pub(crate) const MAX_COMPONENTS: usize = 4;
|
||||
|
||||
/// Maximum image dimensions supported.
|
||||
pub(crate) const MAX_DIMENSIONS: usize = 1 << 27;
|
||||
|
||||
/// Color conversion function that can convert YCbCr colorspace to RGB(A/X) for
|
||||
/// 16 values
|
||||
///
|
||||
/// The following are guarantees to the following functions
|
||||
///
|
||||
/// 1. The `&[i16]` slices passed contain 16 items
|
||||
///
|
||||
/// 2. The slices passed are in the following order
|
||||
/// `y,cb,cr`
|
||||
///
|
||||
/// 3. `&mut [u8]` is zero initialized
|
||||
///
|
||||
/// 4. `&mut usize` points to the position in the array where new values should
|
||||
/// be used
|
||||
///
|
||||
/// The pointer should
|
||||
/// 1. Carry out color conversion
|
||||
/// 2. Update `&mut usize` with the new position
|
||||
|
||||
pub type ColorConvert16Ptr = fn(&[i16; 16], &[i16; 16], &[i16; 16], &mut [u8], &mut usize);
|
||||
|
||||
/// IDCT function prototype
|
||||
///
|
||||
/// This encapsulates a dequantize and IDCT function which will carry out the
|
||||
/// following functions
|
||||
///
|
||||
/// Multiply each 64 element block of `&mut [i16]` with `&Aligned32<[i32;64]>`
|
||||
/// Carry out IDCT (type 3 dct) on ach block of 64 i16's
|
||||
pub type IDCTPtr = fn(&mut [i32; 64], &mut [i16], usize);
|
||||
|
||||
/// An encapsulation of an ICC chunk
|
||||
pub(crate) struct ICCChunk {
|
||||
pub(crate) seq_no: u8,
|
||||
pub(crate) num_markers: u8,
|
||||
pub(crate) data: Vec<u8>
|
||||
}
|
||||
|
||||
/// A JPEG Decoder Instance.
|
||||
#[allow(clippy::upper_case_acronyms, clippy::struct_excessive_bools)]
|
||||
pub struct JpegDecoder<T> {
|
||||
/// Struct to hold image information from SOI
|
||||
pub(crate) info: ImageInfo,
|
||||
/// Quantization tables, will be set to none and the tables will
|
||||
/// be moved to `components` field
|
||||
pub(crate) qt_tables: [Option<[i32; 64]>; MAX_COMPONENTS],
|
||||
/// DC Huffman Tables with a maximum of 4 tables for each component
|
||||
pub(crate) dc_huffman_tables: [Option<HuffmanTable>; MAX_COMPONENTS],
|
||||
/// AC Huffman Tables with a maximum of 4 tables for each component
|
||||
pub(crate) ac_huffman_tables: [Option<HuffmanTable>; MAX_COMPONENTS],
|
||||
/// Image components, holds information like DC prediction and quantization
|
||||
/// tables of a component
|
||||
pub(crate) components: Vec<Components>,
|
||||
/// maximum horizontal component of all channels in the image
|
||||
pub(crate) h_max: usize,
|
||||
// maximum vertical component of all channels in the image
|
||||
pub(crate) v_max: usize,
|
||||
/// mcu's width (interleaved scans)
|
||||
pub(crate) mcu_width: usize,
|
||||
/// MCU height(interleaved scans
|
||||
pub(crate) mcu_height: usize,
|
||||
/// Number of MCU's in the x plane
|
||||
pub(crate) mcu_x: usize,
|
||||
/// Number of MCU's in the y plane
|
||||
pub(crate) mcu_y: usize,
|
||||
/// Is the image interleaved?
|
||||
pub(crate) is_interleaved: bool,
|
||||
/// Image input colorspace, should be YCbCr for a sane image, might be
|
||||
/// grayscale too
|
||||
pub(crate) input_colorspace: ColorSpace,
|
||||
// Progressive image details
|
||||
/// Is the image progressive?
|
||||
pub(crate) is_progressive: bool,
|
||||
|
||||
/// Start of spectral scan
|
||||
pub(crate) spec_start: u8,
|
||||
/// End of spectral scan
|
||||
pub(crate) spec_end: u8,
|
||||
/// Successive approximation bit position high
|
||||
pub(crate) succ_high: u8,
|
||||
/// Successive approximation bit position low
|
||||
pub(crate) succ_low: u8,
|
||||
/// Number of components.
|
||||
pub(crate) num_scans: u8,
|
||||
/// For a scan, check if any component has vertical/horizontal sampling.
|
||||
pub(crate) scan_subsampled: bool,
|
||||
// Function pointers, for pointy stuff.
|
||||
/// Dequantize and idct function
|
||||
// This is determined at runtime which function to run, statically it's
|
||||
// initialized to a platform independent one and during initialization
|
||||
// of this struct, we check if we can switch to a faster one which
|
||||
// depend on certain CPU extensions.
|
||||
pub(crate) idct_func: IDCTPtr,
|
||||
/// Specialized IDCT when we can guarantee only few coefficients are non-zero.
|
||||
///
|
||||
/// **The callee must uphold a contract**. See [`choose_idct_4x4_func`].
|
||||
pub(crate) idct_4x4_func: IDCTPtr,
|
||||
pub(crate) idct_1x1_func: IDCTPtr,
|
||||
// Color convert function which acts on 16 YCbCr values
|
||||
pub(crate) color_convert_16: ColorConvert16Ptr,
|
||||
pub(crate) z_order: [usize; MAX_COMPONENTS],
|
||||
/// restart markers
|
||||
pub(crate) restart_interval: usize,
|
||||
pub(crate) todo: usize,
|
||||
// decoder options
|
||||
pub(crate) options: DecoderOptions,
|
||||
// byte-stream
|
||||
pub(crate) stream: ZReader<T>,
|
||||
// Indicate whether headers have been decoded
|
||||
pub(crate) headers_decoded: bool,
|
||||
pub(crate) seen_sof: bool,
|
||||
|
||||
// exif data, lifted from app2
|
||||
pub(crate) icc_data: Vec<ICCChunk>,
|
||||
pub(crate) is_mjpeg: bool,
|
||||
pub(crate) coeff: usize // Solves some weird bug :)
|
||||
}
|
||||
|
||||
impl<T> JpegDecoder<T>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
#[allow(clippy::redundant_field_names)]
|
||||
fn default(options: DecoderOptions, buffer: T) -> Self {
|
||||
let color_convert = choose_ycbcr_to_rgb_convert_func(ColorSpace::RGB, &options).unwrap();
|
||||
JpegDecoder {
|
||||
info: ImageInfo::default(),
|
||||
qt_tables: [None, None, None, None],
|
||||
dc_huffman_tables: [None, None, None, None],
|
||||
ac_huffman_tables: [None, None, None, None],
|
||||
components: vec![],
|
||||
// Interleaved information
|
||||
h_max: 1,
|
||||
v_max: 1,
|
||||
mcu_height: 0,
|
||||
mcu_width: 0,
|
||||
mcu_x: 0,
|
||||
mcu_y: 0,
|
||||
is_interleaved: false,
|
||||
is_progressive: false,
|
||||
spec_start: 0,
|
||||
spec_end: 0,
|
||||
succ_high: 0,
|
||||
succ_low: 0,
|
||||
num_scans: 0,
|
||||
scan_subsampled: false,
|
||||
idct_func: choose_idct_func(&options),
|
||||
idct_4x4_func: choose_idct_4x4_func(&options),
|
||||
idct_1x1_func: choose_idct_1x1_func(&options),
|
||||
color_convert_16: color_convert,
|
||||
input_colorspace: ColorSpace::YCbCr,
|
||||
z_order: [0; MAX_COMPONENTS],
|
||||
restart_interval: 0,
|
||||
todo: 0x7fff_ffff,
|
||||
options: options,
|
||||
stream: ZReader::new(buffer),
|
||||
headers_decoded: false,
|
||||
seen_sof: false,
|
||||
icc_data: vec![],
|
||||
is_mjpeg: false,
|
||||
coeff: 1
|
||||
}
|
||||
}
|
||||
/// Decode a buffer already in memory
|
||||
///
|
||||
/// The buffer should be a valid jpeg file, perhaps created by the command
|
||||
/// `std:::fs::read()` or a JPEG file downloaded from the internet.
|
||||
///
|
||||
/// # Errors
|
||||
/// See DecodeErrors for an explanation
|
||||
pub fn decode(&mut self) -> Result<Vec<u8>, DecodeErrors> {
|
||||
self.decode_headers()?;
|
||||
let size = self.output_buffer_size().unwrap();
|
||||
let mut out = vec![0; size];
|
||||
self.decode_into(&mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Create a new Decoder instance
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `stream`: The raw bytes of a jpeg file.
|
||||
#[must_use]
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(stream: T) -> JpegDecoder<T> {
|
||||
JpegDecoder::default(DecoderOptions::default(), stream)
|
||||
}
|
||||
|
||||
/// Returns the image information
|
||||
///
|
||||
/// This **must** be called after a subsequent call to [`decode`] or [`decode_headers`]
|
||||
/// it will return `None`
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(info)`: Image information,width, height, number of components
|
||||
/// - None: Indicates image headers haven't been decoded
|
||||
///
|
||||
/// [`decode`]: JpegDecoder::decode
|
||||
/// [`decode_headers`]: JpegDecoder::decode_headers
|
||||
#[must_use]
|
||||
pub fn info(&self) -> Option<ImageInfo> {
|
||||
// we check for fails to that call by comparing what we have to the default, if
|
||||
// it's default we assume that the caller failed to uphold the
|
||||
// guarantees. We can be sure that an image cannot be the default since
|
||||
// its a hard panic in-case width or height are set to zero.
|
||||
if !self.headers_decoded {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(self.info.clone());
|
||||
}
|
||||
|
||||
/// Return the number of bytes required to hold a decoded image frame
|
||||
/// decoded using the given input transformations
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(usize)`: Minimum size for a buffer needed to decode the image
|
||||
/// - `None`: Indicates the image was not decoded, or image dimensions would overflow a usize
|
||||
///
|
||||
#[must_use]
|
||||
pub fn output_buffer_size(&self) -> Option<usize> {
|
||||
return if self.headers_decoded {
|
||||
Some(
|
||||
usize::from(self.width())
|
||||
.checked_mul(usize::from(self.height()))?
|
||||
.checked_mul(self.options.jpeg_get_out_colorspace().num_components())?
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
/// Get an immutable reference to the decoder options
|
||||
/// for the decoder instance
|
||||
///
|
||||
/// This can be used to modify options before actual decoding
|
||||
/// but after initial creation
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use zune_core::bytestream::ZCursor;
|
||||
/// use zune_jpeg::JpegDecoder;
|
||||
///
|
||||
/// let mut decoder = JpegDecoder::new(ZCursor::new(&[]));
|
||||
/// // get current options
|
||||
/// let mut options = decoder.options();
|
||||
/// // modify it
|
||||
/// let new_options = options.set_max_width(10);
|
||||
/// // set it back
|
||||
/// decoder.set_options(new_options);
|
||||
///
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn options(&self) -> &DecoderOptions {
|
||||
&self.options
|
||||
}
|
||||
/// Return the input colorspace of the image
|
||||
///
|
||||
/// This indicates the colorspace that is present in
|
||||
/// the image, but this may be different to the colorspace that
|
||||
/// the output will be transformed to
|
||||
///
|
||||
/// # Returns
|
||||
/// -`Some(Colorspace)`: Input colorspace
|
||||
/// - None : Indicates the headers weren't decoded
|
||||
#[must_use]
|
||||
pub fn input_colorspace(&self) -> Option<ColorSpace> {
|
||||
return if self.headers_decoded { Some(self.input_colorspace) } else { None };
|
||||
}
|
||||
/// Set decoder options
|
||||
///
|
||||
/// This can be used to set new options even after initialization
|
||||
/// but before decoding.
|
||||
///
|
||||
/// This does not bear any significance after decoding an image
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `options`: New decoder options
|
||||
///
|
||||
/// # Example
|
||||
/// Set maximum jpeg progressive passes to be 4
|
||||
///
|
||||
/// ```no_run
|
||||
/// use zune_core::bytestream::ZCursor;
|
||||
/// use zune_jpeg::JpegDecoder;
|
||||
/// let mut decoder =JpegDecoder::new(ZCursor::new(&[]));
|
||||
/// // this works also because DecoderOptions implements `Copy`
|
||||
/// let options = decoder.options().jpeg_set_max_scans(4);
|
||||
/// // set the new options
|
||||
/// decoder.set_options(options);
|
||||
/// // now decode
|
||||
/// decoder.decode().unwrap();
|
||||
/// ```
|
||||
pub fn set_options(&mut self, options: DecoderOptions) {
|
||||
self.options = options;
|
||||
}
|
||||
/// Decode Decoder headers
|
||||
///
|
||||
/// This routine takes care of parsing supported headers from a Decoder
|
||||
/// image
|
||||
///
|
||||
/// # Supported Headers
|
||||
/// - APP(0)
|
||||
/// - SOF(O)
|
||||
/// - DQT -> Quantization tables
|
||||
/// - DHT -> Huffman tables
|
||||
/// - SOS -> Start of Scan
|
||||
/// # Unsupported Headers
|
||||
/// - SOF(n) -> Decoder images which are not baseline/progressive
|
||||
/// - DAC -> Images using Arithmetic tables
|
||||
/// - JPG(n)
|
||||
fn decode_headers_internal(&mut self) -> Result<(), DecodeErrors> {
|
||||
if self.headers_decoded {
|
||||
trace!("Headers decoded!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// match output colorspace here
|
||||
// we know this will only be called once per image
|
||||
// so makes sense
|
||||
// We only care for ycbcr to rgb/rgba here
|
||||
// in case one is using another colorspace.
|
||||
// May god help you
|
||||
let out_colorspace = self.options.jpeg_get_out_colorspace();
|
||||
|
||||
if matches!(
|
||||
out_colorspace,
|
||||
ColorSpace::BGR | ColorSpace::BGRA | ColorSpace::RGB | ColorSpace::RGBA
|
||||
) {
|
||||
self.color_convert_16 = choose_ycbcr_to_rgb_convert_func(
|
||||
self.options.jpeg_get_out_colorspace(),
|
||||
&self.options
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// First two bytes should be jpeg soi marker
|
||||
let magic_bytes = self.stream.get_u16_be_err()?;
|
||||
|
||||
let mut last_byte = 0;
|
||||
let mut bytes_before_marker = 0;
|
||||
|
||||
if magic_bytes != 0xffd8 {
|
||||
return Err(DecodeErrors::IllegalMagicBytes(magic_bytes));
|
||||
}
|
||||
|
||||
loop {
|
||||
// read a byte
|
||||
let mut m = self.stream.read_u8_err()?;
|
||||
|
||||
// AND OF COURSE some images will have fill bytes in their marker
|
||||
// bitstreams because why not.
|
||||
//
|
||||
// I am disappointed as a man.
|
||||
if (m == 0xFF || m == 0) && last_byte == 0xFF {
|
||||
// This handles the edge case where
|
||||
// images have markers with fill bytes(0xFF)
|
||||
// or byte stuffing (0)
|
||||
// I.e 0xFF 0xFF 0xDA
|
||||
// and
|
||||
// 0xFF 0 0xDA
|
||||
// It should ignore those fill bytes and take 0xDA
|
||||
// I don't know why such images exist
|
||||
// but they do.
|
||||
// so this is for you (with love)
|
||||
while m == 0xFF || m == 0x0 {
|
||||
last_byte = m;
|
||||
m = self.stream.read_u8_err()?;
|
||||
}
|
||||
}
|
||||
// Last byte should be 0xFF to confirm existence of a marker since markers look
|
||||
// like OxFF(some marker data)
|
||||
if last_byte == 0xFF {
|
||||
let marker = Marker::from_u8(m);
|
||||
if let Some(n) = marker {
|
||||
if bytes_before_marker > 3 {
|
||||
if self.options.strict_mode()
|
||||
/*No reason to use this*/
|
||||
{
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"[strict-mode]: Extra bytes between headers"
|
||||
));
|
||||
}
|
||||
|
||||
error!(
|
||||
"Extra bytes {} before marker 0xFF{:X}",
|
||||
bytes_before_marker - 3,
|
||||
m
|
||||
);
|
||||
}
|
||||
|
||||
bytes_before_marker = 0;
|
||||
|
||||
self.parse_marker_inner(n)?;
|
||||
|
||||
// break after reading the start of scan.
|
||||
// what follows is the image data
|
||||
if n == Marker::SOS {
|
||||
self.headers_decoded = true;
|
||||
trace!("Input colorspace {:?}", self.input_colorspace);
|
||||
|
||||
// Check if image is RGB
|
||||
// The check is weird, we need to check if ID
|
||||
// represents R, G and B in ascii,
|
||||
//
|
||||
// I am not sure if this is even specified in any standard,
|
||||
// but jpegli https://github.com/google/jpegli does encode
|
||||
// its images that way, so this will check for that. and handle it appropriately
|
||||
// It is spefified here so that on a successful header decode,we can at least
|
||||
// try to attribute image colorspace correctly.
|
||||
//
|
||||
// It was first the issue in https://github.com/etemesi254/zune-image/issues/291
|
||||
// that brought it to light
|
||||
//
|
||||
let mut is_rgb = self.components.len() == 3;
|
||||
let chars = ['R', 'G', 'B'];
|
||||
for (comp, single_char) in self.components.iter().zip(chars.iter()) {
|
||||
is_rgb &= comp.id == (*single_char) as u8
|
||||
}
|
||||
// Image is RGB, change colorspace
|
||||
if is_rgb {
|
||||
self.input_colorspace = ColorSpace::RGB;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
bytes_before_marker = 0;
|
||||
|
||||
warn!("Marker 0xFF{:X} not known", m);
|
||||
|
||||
let length = self.stream.get_u16_be_err()?;
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Found a marker with invalid length : {length}"
|
||||
)));
|
||||
}
|
||||
|
||||
warn!("Skipping {} bytes", length - 2);
|
||||
self.stream.skip((length - 2) as usize)?;
|
||||
}
|
||||
}
|
||||
last_byte = m;
|
||||
bytes_before_marker += 1;
|
||||
}
|
||||
// Check if image is RGB
|
||||
}
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) fn parse_marker_inner(&mut self, m: Marker) -> Result<(), DecodeErrors> {
|
||||
match m {
|
||||
Marker::SOF(0..=2) => {
|
||||
let marker = {
|
||||
// choose marker
|
||||
if m == Marker::SOF(0) || m == Marker::SOF(1) {
|
||||
SOFMarkers::BaselineDct
|
||||
} else {
|
||||
self.is_progressive = true;
|
||||
SOFMarkers::ProgressiveDctHuffman
|
||||
}
|
||||
};
|
||||
|
||||
trace!("Image encoding scheme =`{:?}`", marker);
|
||||
// get components
|
||||
parse_start_of_frame(marker, self)?;
|
||||
}
|
||||
// Start of Frame Segments not supported
|
||||
Marker::SOF(v) => {
|
||||
let feature = UnsupportedSchemes::from_int(v);
|
||||
|
||||
if let Some(feature) = feature {
|
||||
return Err(DecodeErrors::Unsupported(feature));
|
||||
}
|
||||
|
||||
return Err(DecodeErrors::Format("Unsupported image format".to_string()));
|
||||
}
|
||||
//APP(0) segment
|
||||
Marker::APP(0) => {
|
||||
let mut length = self.stream.get_u16_be_err()?;
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Found a marker with invalid length:{length}\n"
|
||||
)));
|
||||
}
|
||||
// skip for now
|
||||
if length > 5 {
|
||||
let mut buffer = [0u8; 5];
|
||||
self.stream.read_exact_bytes(&mut buffer)?;
|
||||
if &buffer == b"AVI1\0" {
|
||||
self.is_mjpeg = true;
|
||||
}
|
||||
length -= 5;
|
||||
}
|
||||
|
||||
self.stream.skip(length.saturating_sub(2) as usize)?;
|
||||
|
||||
//parse_app(buf, m, &mut self.info)?;
|
||||
}
|
||||
Marker::APP(1) => {
|
||||
parse_app1(self)?;
|
||||
}
|
||||
|
||||
Marker::APP(2) => {
|
||||
parse_app2(self)?;
|
||||
}
|
||||
// Quantization tables
|
||||
Marker::DQT => {
|
||||
parse_dqt(self)?;
|
||||
}
|
||||
// Huffman tables
|
||||
Marker::DHT => {
|
||||
parse_huffman(self)?;
|
||||
}
|
||||
// Start of Scan Data
|
||||
Marker::SOS => {
|
||||
parse_sos(self)?;
|
||||
}
|
||||
Marker::EOI => return Err(DecodeErrors::FormatStatic("Premature End of image")),
|
||||
|
||||
Marker::DAC | Marker::DNL => {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Parsing of the following header `{m:?}` is not supported,\
|
||||
cannot continue"
|
||||
)));
|
||||
}
|
||||
Marker::DRI => {
|
||||
if self.stream.get_u16_be_err()? != 4 {
|
||||
return Err(DecodeErrors::Format(
|
||||
"Bad DRI length, Corrupt JPEG".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
self.restart_interval = usize::from(self.stream.get_u16_be_err()?);
|
||||
trace!("DRI marker present ({})", self.restart_interval);
|
||||
|
||||
self.todo = self.restart_interval;
|
||||
}
|
||||
Marker::APP(14) => {
|
||||
parse_app14(self)?;
|
||||
}
|
||||
Marker::APP(13) => {
|
||||
parse_app13(self)?;
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Capabilities for processing marker \"{:?}\" not implemented",
|
||||
m
|
||||
);
|
||||
|
||||
let length = self.stream.get_u16_be_err()?;
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Found a marker with invalid length:{length}\n"
|
||||
)));
|
||||
}
|
||||
warn!("Skipping {} bytes", length - 2);
|
||||
self.stream.skip((length - 2) as usize)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Get the embedded ICC profile if it exists
|
||||
/// and is correct
|
||||
///
|
||||
/// One needs not to decode the whole image to extract this,
|
||||
/// calling [`decode_headers`] for an image with an ICC profile
|
||||
/// allows you to decode this
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(Vec<u8>)`: The raw ICC profile of the image
|
||||
/// - `None`: May indicate an error in the ICC profile , non-existence of
|
||||
/// an ICC profile, or that the headers weren't decoded.
|
||||
///
|
||||
/// [`decode_headers`]:Self::decode_headers
|
||||
#[must_use]
|
||||
pub fn icc_profile(&self) -> Option<Vec<u8>> {
|
||||
let mut marker_present: [Option<&ICCChunk>; 256] = [None; 256];
|
||||
|
||||
if !self.headers_decoded {
|
||||
return None;
|
||||
}
|
||||
let num_markers = self.icc_data.len();
|
||||
|
||||
if num_markers == 0 || num_markers >= 255 {
|
||||
return None;
|
||||
}
|
||||
// check validity
|
||||
for chunk in &self.icc_data {
|
||||
if usize::from(chunk.num_markers) != num_markers {
|
||||
// all the lengths must match
|
||||
return None;
|
||||
}
|
||||
if chunk.seq_no == 0 {
|
||||
warn!("Zero sequence number in ICC, corrupt ICC chunk");
|
||||
return None;
|
||||
}
|
||||
if marker_present[usize::from(chunk.seq_no)].is_some() {
|
||||
// duplicate seq_no
|
||||
warn!("Duplicate sequence number in ICC, corrupt chunk");
|
||||
return None;
|
||||
}
|
||||
|
||||
marker_present[usize::from(chunk.seq_no)] = Some(chunk);
|
||||
}
|
||||
let mut data = Vec::with_capacity(1000);
|
||||
// assemble the data now
|
||||
for chunk in marker_present.get(1..=num_markers).unwrap() {
|
||||
if let Some(ch) = chunk {
|
||||
data.extend_from_slice(&ch.data);
|
||||
} else {
|
||||
warn!("Missing icc sequence number, corrupt ICC chunk ");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(data)
|
||||
}
|
||||
/// Return the exif data for the file
|
||||
///
|
||||
/// This returns the raw exif data starting at the
|
||||
/// TIFF header
|
||||
///
|
||||
/// # Returns
|
||||
/// -`Some(data)`: The raw exif data, if present in the image
|
||||
/// - None: May indicate the following
|
||||
///
|
||||
/// 1. The image doesn't have exif data
|
||||
/// 2. The image headers haven't been decoded
|
||||
#[must_use]
|
||||
pub fn exif(&self) -> Option<&Vec<u8>> {
|
||||
return self.info.exif_data.as_ref();
|
||||
}
|
||||
/// Return the XMP data for the file
|
||||
///
|
||||
/// This returns raw XMP data starting at the XML header
|
||||
/// One needs an XML/XMP decoder to extract valuable metadata
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(data)`: Raw xmp data
|
||||
/// - `None`: May indicate the following
|
||||
/// 1. The image does not have xmp data
|
||||
/// 2. The image headers have not been decoded
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use zune_core::bytestream::ZCursor;
|
||||
/// use zune_jpeg::JpegDecoder;
|
||||
/// let mut decoder = JpegDecoder::new(ZCursor::new(&[]));
|
||||
/// // decode headers to extract xmp metadata if present
|
||||
/// decoder.decode_headers().unwrap();
|
||||
/// if let Some(data) = decoder.xmp(){
|
||||
/// let stringified = String::from_utf8_lossy(data);
|
||||
/// println!("XMP")
|
||||
/// } else{
|
||||
/// println!("No XMP Found")
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
pub fn xmp(&self) -> Option<&Vec<u8>> {
|
||||
return self.info.xmp_data.as_ref();
|
||||
}
|
||||
/// Return the IPTC data for the file
|
||||
///
|
||||
/// This returns the raw IPTC data.
|
||||
///
|
||||
/// # Returns
|
||||
/// -`Some(data)`: The raw IPTC data, if present in the image
|
||||
/// - None: May indicate the following
|
||||
///
|
||||
/// 1. The image doesn't have IPTC data
|
||||
/// 2. The image headers haven't been decoded
|
||||
#[must_use]
|
||||
pub fn iptc(&self) -> Option<&Vec<u8>> {
|
||||
return self.info.iptc_data.as_ref();
|
||||
}
|
||||
/// Get the output colorspace the image pixels will be decoded into
|
||||
///
|
||||
///
|
||||
/// # Note.
|
||||
/// This field can only be regarded after decoding headers,
|
||||
/// as markers such as Adobe APP14 may dictate different colorspaces
|
||||
/// than requested.
|
||||
///
|
||||
/// Calling `decode_headers` is sufficient to know what colorspace the
|
||||
/// output is, if this is called after `decode` it indicates the colorspace
|
||||
/// the output is currently in
|
||||
///
|
||||
/// Additionally not all input->output colorspace mappings are supported
|
||||
/// but all input colorspaces can map to RGB colorspace, so that's a safe bet
|
||||
/// if one is handling image formats
|
||||
///
|
||||
///# Returns
|
||||
/// - `Some(Colorspace)`: If headers have been decoded, the colorspace the
|
||||
///output array will be in
|
||||
///- `None
|
||||
#[must_use]
|
||||
pub fn output_colorspace(&self) -> Option<ColorSpace> {
|
||||
return if self.headers_decoded {
|
||||
Some(self.options.jpeg_get_out_colorspace())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
/// Decode into a pre-allocated buffer
|
||||
///
|
||||
/// It is an error if the buffer size is smaller than
|
||||
/// [`output_buffer_size()`](Self::output_buffer_size)
|
||||
///
|
||||
/// If the buffer is bigger than expected, we ignore the end padding bytes
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// - Read headers and then alloc a buffer big enough to hold the image
|
||||
///
|
||||
/// ```no_run
|
||||
/// use zune_core::bytestream::ZCursor;
|
||||
/// use zune_jpeg::JpegDecoder;
|
||||
/// let mut decoder = JpegDecoder::new(ZCursor::new(&[]));
|
||||
/// // before we get output, we must decode the headers to get width
|
||||
/// // height, and input colorspace
|
||||
/// decoder.decode_headers().unwrap();
|
||||
///
|
||||
/// let mut out = vec![0;decoder.output_buffer_size().unwrap()];
|
||||
/// // write into out
|
||||
/// decoder.decode_into(&mut out).unwrap();
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
pub fn decode_into(&mut self, out: &mut [u8]) -> Result<(), DecodeErrors> {
|
||||
self.decode_headers_internal()?;
|
||||
|
||||
let expected_size = self.output_buffer_size().unwrap();
|
||||
|
||||
if out.len() < expected_size {
|
||||
// too small of a size
|
||||
return Err(DecodeErrors::TooSmallOutput(expected_size, out.len()));
|
||||
}
|
||||
|
||||
// ensure we don't touch anyone else's scratch space
|
||||
let out_len = core::cmp::min(out.len(), expected_size);
|
||||
let out = &mut out[0..out_len];
|
||||
|
||||
if self.is_progressive {
|
||||
self.decode_mcu_ycbcr_progressive(out)
|
||||
} else {
|
||||
self.decode_mcu_ycbcr_baseline(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read only headers from a jpeg image buffer
|
||||
///
|
||||
/// This allows you to extract important information like
|
||||
/// image width and height without decoding the full image
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// use zune_core::bytestream::ZCursor;
|
||||
/// use zune_jpeg::{JpegDecoder};
|
||||
///
|
||||
/// let img_data = std::fs::read("a_valid.jpeg").unwrap();
|
||||
/// let mut decoder = JpegDecoder::new(ZCursor::new(&img_data));
|
||||
/// decoder.decode_headers().unwrap();
|
||||
///
|
||||
/// println!("Total decoder dimensions are : {:?} pixels",decoder.dimensions());
|
||||
/// println!("Number of components in the image are {}", decoder.info().unwrap().components);
|
||||
/// ```
|
||||
/// # Errors
|
||||
/// See DecodeErrors enum for list of possible errors during decoding
|
||||
pub fn decode_headers(&mut self) -> Result<(), DecodeErrors> {
|
||||
self.decode_headers_internal()?;
|
||||
Ok(())
|
||||
}
|
||||
/// Create a new decoder with the specified options to be used for decoding
|
||||
/// an image
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `buf`: The input buffer from where we will pull in compressed jpeg bytes from
|
||||
/// - `options`: Options specific to this decoder instance
|
||||
#[must_use]
|
||||
pub fn new_with_options(buf: T, options: DecoderOptions) -> JpegDecoder<T> {
|
||||
JpegDecoder::default(options, buf)
|
||||
}
|
||||
|
||||
/// Set up-sampling routines in case an image is down sampled
|
||||
pub(crate) fn set_upsampling(&mut self) -> Result<(), DecodeErrors> {
|
||||
// no sampling, return early
|
||||
// check if horizontal max ==1
|
||||
if self.h_max == self.v_max && self.h_max == 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for comp in &mut self.components {
|
||||
let hs = self.h_max / comp.horizontal_sample;
|
||||
let vs = self.v_max / comp.vertical_sample;
|
||||
|
||||
let samp_factor = match (hs, vs) {
|
||||
(1, 1) => {
|
||||
comp.sample_ratio = SampleRatios::None;
|
||||
upsample_no_op
|
||||
}
|
||||
(2, 1) => {
|
||||
comp.sample_ratio = SampleRatios::H;
|
||||
choose_horizontal_samp_function(&self.options)
|
||||
}
|
||||
(1, 2) => {
|
||||
comp.sample_ratio = SampleRatios::V;
|
||||
choose_v_samp_function(&self.options)
|
||||
}
|
||||
(2, 2) => {
|
||||
comp.sample_ratio = SampleRatios::HV;
|
||||
choose_hv_samp_function(&self.options)
|
||||
}
|
||||
(hs, vs) => {
|
||||
comp.sample_ratio = SampleRatios::Generic(hs, vs);
|
||||
generic_sampler()
|
||||
}
|
||||
};
|
||||
comp.setup_upsample_scanline();
|
||||
comp.up_sampler = samp_factor;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
#[must_use]
|
||||
/// Get the width of the image as a u16
|
||||
///
|
||||
/// The width lies between 1 and 65535
|
||||
pub(crate) fn width(&self) -> u16 {
|
||||
self.info.width
|
||||
}
|
||||
|
||||
/// Get the height of the image as a u16
|
||||
///
|
||||
/// The height lies between 1 and 65535
|
||||
#[must_use]
|
||||
pub(crate) fn height(&self) -> u16 {
|
||||
self.info.height
|
||||
}
|
||||
|
||||
/// Get image dimensions as a tuple of width and height
|
||||
/// or `None` if the image hasn't been decoded.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(width,height)`: Image dimensions
|
||||
/// - None : The image headers haven't been decoded
|
||||
#[must_use]
|
||||
pub const fn dimensions(&self) -> Option<(usize, usize)> {
|
||||
return if self.headers_decoded {
|
||||
Some((self.info.width as usize, self.info.height as usize))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, Debug)]
|
||||
pub struct GainMapInfo {
|
||||
pub data: Vec<u8>
|
||||
}
|
||||
/// A struct representing Image Information
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct ImageInfo {
|
||||
/// Width of the image
|
||||
pub width: u16,
|
||||
/// Height of image
|
||||
pub height: u16,
|
||||
/// PixelDensity
|
||||
pub pixel_density: u8,
|
||||
/// Start of frame markers
|
||||
pub sof: SOFMarkers,
|
||||
/// Horizontal sample
|
||||
pub x_density: u16,
|
||||
/// Vertical sample
|
||||
pub y_density: u16,
|
||||
/// Number of components
|
||||
pub components: u8,
|
||||
/// Gain Map information, useful for
|
||||
/// UHDR images
|
||||
pub gain_map_info: Vec<GainMapInfo>,
|
||||
/// Multi picture information, useful for
|
||||
/// UHDR images
|
||||
pub multi_picture_information: Option<Vec<u8>>,
|
||||
/// Exif Data
|
||||
pub exif_data: Option<Vec<u8>>,
|
||||
/// XMP Data
|
||||
pub xmp_data: Option<Vec<u8>>,
|
||||
/// IPTC Data
|
||||
pub iptc_data: Option<Vec<u8>>,
|
||||
/// Image sub-sampling ratio
|
||||
pub sample_ratio: SampleRatios
|
||||
}
|
||||
|
||||
impl ImageInfo {
|
||||
/// Set width of the image
|
||||
///
|
||||
/// Found in the start of frame
|
||||
|
||||
pub(crate) fn set_width(&mut self, width: u16) {
|
||||
self.width = width;
|
||||
}
|
||||
|
||||
/// Set height of the image
|
||||
///
|
||||
/// Found in the start of frame
|
||||
|
||||
pub(crate) fn set_height(&mut self, height: u16) {
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Set the image density
|
||||
///
|
||||
/// Found in the start of frame
|
||||
|
||||
pub(crate) fn set_density(&mut self, density: u8) {
|
||||
self.pixel_density = density;
|
||||
}
|
||||
|
||||
/// Set image Start of frame marker
|
||||
///
|
||||
/// found in the Start of frame header
|
||||
|
||||
pub(crate) fn set_sof_marker(&mut self, marker: SOFMarkers) {
|
||||
self.sof = marker;
|
||||
}
|
||||
|
||||
/// Set image x-density(dots per pixel)
|
||||
///
|
||||
/// Found in the APP(0) marker
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_x(&mut self, sample: u16) {
|
||||
self.x_density = sample;
|
||||
}
|
||||
|
||||
/// Set image y-density
|
||||
///
|
||||
/// Found in the APP(0) marker
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_y(&mut self, sample: u16) {
|
||||
self.y_density = sample;
|
||||
}
|
||||
}
|
||||
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Contains most common errors that may be encountered in decoding a Decoder
|
||||
//! image
|
||||
|
||||
use alloc::string::String;
|
||||
use core::fmt::{Debug, Display, Formatter};
|
||||
|
||||
use zune_core::bytestream::ZByteIoError;
|
||||
|
||||
use crate::misc::{
|
||||
START_OF_FRAME_EXT_AR, START_OF_FRAME_EXT_SEQ, START_OF_FRAME_LOS_SEQ,
|
||||
START_OF_FRAME_LOS_SEQ_AR, START_OF_FRAME_PROG_DCT_AR
|
||||
};
|
||||
|
||||
/// Common Decode errors
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub enum DecodeErrors {
|
||||
/// Any other thing we do not know
|
||||
Format(String),
|
||||
/// Any other thing we do not know but we
|
||||
/// don't need to allocate space on the heap
|
||||
FormatStatic(&'static str),
|
||||
/// Illegal Magic Bytes
|
||||
IllegalMagicBytes(u16),
|
||||
/// problems with the Huffman Tables in a Decoder file
|
||||
HuffmanDecode(String),
|
||||
/// Image has zero width
|
||||
ZeroError,
|
||||
/// Discrete Quantization Tables error
|
||||
DqtError(String),
|
||||
/// Start of scan errors
|
||||
SosError(String),
|
||||
/// Start of frame errors
|
||||
SofError(String),
|
||||
/// UnsupportedImages
|
||||
Unsupported(UnsupportedSchemes),
|
||||
/// MCU errors
|
||||
MCUError(String),
|
||||
/// Exhausted data
|
||||
ExhaustedData,
|
||||
/// Large image dimensions(Corrupted data)?
|
||||
LargeDimensions(usize),
|
||||
/// Too small output for size
|
||||
TooSmallOutput(usize, usize),
|
||||
|
||||
IoErrors(ZByteIoError)
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for DecodeErrors {}
|
||||
|
||||
impl From<&'static str> for DecodeErrors {
|
||||
fn from(data: &'static str) -> Self {
|
||||
return Self::FormatStatic(data);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ZByteIoError> for DecodeErrors {
|
||||
fn from(data: ZByteIoError) -> Self {
|
||||
return Self::IoErrors(data);
|
||||
}
|
||||
}
|
||||
impl Debug for DecodeErrors {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
match &self
|
||||
{
|
||||
Self::Format(ref a) => write!(f, "{a:?}"),
|
||||
Self::FormatStatic(a) => write!(f, "{:?}", &a),
|
||||
|
||||
Self::HuffmanDecode(ref reason) =>
|
||||
{
|
||||
write!(f, "Error decoding huffman values: {reason}")
|
||||
}
|
||||
Self::ZeroError => write!(f, "Image width or height is set to zero, cannot continue"),
|
||||
Self::DqtError(ref reason) => write!(f, "Error parsing DQT segment. Reason:{reason}"),
|
||||
Self::SosError(ref reason) => write!(f, "Error parsing SOS Segment. Reason:{reason}"),
|
||||
Self::SofError(ref reason) => write!(f, "Error parsing SOF segment. Reason:{reason}"),
|
||||
Self::IllegalMagicBytes(bytes) =>
|
||||
{
|
||||
write!(f, "Error parsing image. Illegal start bytes:{bytes:X}")
|
||||
}
|
||||
Self::MCUError(ref reason) => write!(f, "Error in decoding MCU. Reason {reason}"),
|
||||
Self::Unsupported(ref image_type) =>
|
||||
{
|
||||
write!(f, "{image_type:?}")
|
||||
}
|
||||
Self::ExhaustedData => write!(f, "Exhausted data in the image"),
|
||||
Self::LargeDimensions(ref dimensions) => write!(
|
||||
f,
|
||||
"Too large dimensions {dimensions},library supports up to {}", crate::decoder::MAX_DIMENSIONS
|
||||
),
|
||||
Self::TooSmallOutput(expected, found) => write!(f, "Too small output, expected buffer with at least {expected} bytes but got one with {found} bytes"),
|
||||
Self::IoErrors(error)=>write!(f,"I/O errors {error:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DecodeErrors {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains Unsupported/Yet-to-be supported Decoder image encoding types.
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
pub enum UnsupportedSchemes {
|
||||
/// SOF_1 Extended sequential DCT,Huffman coding
|
||||
ExtendedSequentialHuffman,
|
||||
/// Lossless (sequential), huffman coding,
|
||||
LosslessHuffman,
|
||||
/// Extended sequential DEC, arithmetic coding
|
||||
ExtendedSequentialDctArithmetic,
|
||||
/// Progressive DCT, arithmetic coding,
|
||||
ProgressiveDctArithmetic,
|
||||
/// Lossless ( sequential), arithmetic coding
|
||||
LosslessArithmetic
|
||||
}
|
||||
|
||||
impl Debug for UnsupportedSchemes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
match &self {
|
||||
Self::ExtendedSequentialHuffman => {
|
||||
write!(f, "The library cannot yet decode images encoded using Extended Sequential Huffman encoding scheme yet.")
|
||||
}
|
||||
Self::LosslessHuffman => {
|
||||
write!(f, "The library cannot yet decode images encoded with Lossless Huffman encoding scheme")
|
||||
}
|
||||
Self::ExtendedSequentialDctArithmetic => {
|
||||
write!(f,"The library cannot yet decode Images Encoded with Extended Sequential DCT Arithmetic scheme")
|
||||
}
|
||||
Self::ProgressiveDctArithmetic => {
|
||||
write!(f,"The library cannot yet decode images encoded with Progressive DCT Arithmetic scheme")
|
||||
}
|
||||
Self::LosslessArithmetic => {
|
||||
write!(f,"The library cannot yet decode images encoded with Lossless Arithmetic encoding scheme")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UnsupportedSchemes {
|
||||
#[must_use]
|
||||
/// Create an unsupported scheme from an integer
|
||||
///
|
||||
/// # Returns
|
||||
/// `Some(UnsupportedScheme)` if the int refers to a specific scheme,
|
||||
/// otherwise returns `None`
|
||||
pub fn from_int(int: u8) -> Option<UnsupportedSchemes> {
|
||||
let int = u16::from_be_bytes([0xff, int]);
|
||||
|
||||
match int {
|
||||
START_OF_FRAME_PROG_DCT_AR => Some(Self::ProgressiveDctArithmetic),
|
||||
START_OF_FRAME_LOS_SEQ => Some(Self::LosslessHuffman),
|
||||
START_OF_FRAME_LOS_SEQ_AR => Some(Self::LosslessArithmetic),
|
||||
START_OF_FRAME_EXT_SEQ => Some(Self::ExtendedSequentialHuffman),
|
||||
START_OF_FRAME_EXT_AR => Some(Self::ExtendedSequentialDctArithmetic),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+662
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Decode Decoder markers/segments
|
||||
//!
|
||||
//! This file deals with decoding header information in a jpeg file
|
||||
//!
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use zune_core::bytestream::ZByteReaderTrait;
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::log::{debug, trace, warn};
|
||||
|
||||
use core::cmp::max;
|
||||
|
||||
use crate::components::{Components, SampleRatios};
|
||||
use crate::decoder::{GainMapInfo, ICCChunk, JpegDecoder, MAX_COMPONENTS};
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::huffman::HuffmanTable;
|
||||
use crate::misc::{SOFMarkers, UN_ZIGZAG};
|
||||
|
||||
///**B.2.4.2 Huffman table-specification syntax**
|
||||
#[allow(clippy::similar_names, clippy::cast_sign_loss)]
|
||||
pub(crate) fn parse_huffman<T: ZByteReaderTrait>(
|
||||
decoder: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors>
|
||||
where
|
||||
{
|
||||
// Read the length of the Huffman table
|
||||
let mut dht_length = i32::from(decoder.stream.get_u16_be_err()?.checked_sub(2).ok_or(
|
||||
DecodeErrors::FormatStatic("Invalid Huffman length in image")
|
||||
)?);
|
||||
|
||||
while dht_length > 16 {
|
||||
// HT information
|
||||
let ht_info = decoder.stream.read_u8_err()?;
|
||||
// third bit indicates whether the huffman encoding is DC or AC type
|
||||
let dc_or_ac = (ht_info >> 4) & 0xF;
|
||||
// Indicate the position of this table, should be less than 4;
|
||||
let index = (ht_info & 0xF) as usize;
|
||||
// read the number of symbols
|
||||
let mut num_symbols: [u8; 17] = [0; 17];
|
||||
|
||||
if index >= MAX_COMPONENTS {
|
||||
return Err(DecodeErrors::HuffmanDecode(format!(
|
||||
"Invalid DHT index {index}, expected between 0 and 3"
|
||||
)));
|
||||
}
|
||||
|
||||
if dc_or_ac > 1 {
|
||||
return Err(DecodeErrors::HuffmanDecode(format!(
|
||||
"Invalid DHT position {dc_or_ac}, should be 0 or 1"
|
||||
)));
|
||||
}
|
||||
|
||||
decoder.stream.read_exact_bytes(&mut num_symbols[1..17])?;
|
||||
|
||||
dht_length -= 1 + 16;
|
||||
|
||||
let symbols_sum: i32 = num_symbols.iter().map(|f| i32::from(*f)).sum();
|
||||
|
||||
// The sum of the number of symbols cannot be greater than 256;
|
||||
if symbols_sum > 256 {
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Encountered Huffman table with excessive length in DHT"
|
||||
));
|
||||
}
|
||||
if symbols_sum > dht_length {
|
||||
return Err(DecodeErrors::HuffmanDecode(format!(
|
||||
"Excessive Huffman table of length {symbols_sum} found when header length is {dht_length}"
|
||||
)));
|
||||
}
|
||||
dht_length -= symbols_sum;
|
||||
// A table containing symbols in increasing code length
|
||||
let mut symbols = [0; 256];
|
||||
|
||||
decoder
|
||||
.stream
|
||||
.read_exact_bytes(&mut symbols[0..(symbols_sum as usize)])?;
|
||||
// store
|
||||
match dc_or_ac {
|
||||
0 => {
|
||||
decoder.dc_huffman_tables[index] = Some(HuffmanTable::new(
|
||||
&num_symbols,
|
||||
symbols,
|
||||
true,
|
||||
decoder.is_progressive
|
||||
)?);
|
||||
}
|
||||
_ => {
|
||||
decoder.ac_huffman_tables[index] = Some(HuffmanTable::new(
|
||||
&num_symbols,
|
||||
symbols,
|
||||
false,
|
||||
decoder.is_progressive
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dht_length > 0 {
|
||||
return Err(DecodeErrors::FormatStatic("Bogus Huffman table definition"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///**B.2.4.1 Quantization table-specification syntax**
|
||||
#[allow(clippy::cast_possible_truncation, clippy::needless_range_loop)]
|
||||
pub(crate) fn parse_dqt<T: ZByteReaderTrait>(img: &mut JpegDecoder<T>) -> Result<(), DecodeErrors> {
|
||||
// read length
|
||||
let mut qt_length =
|
||||
img.stream
|
||||
.get_u16_be_err()?
|
||||
.checked_sub(2)
|
||||
.ok_or(DecodeErrors::FormatStatic(
|
||||
"Invalid DQT length. Length should be greater than 2"
|
||||
))?;
|
||||
// A single DQT header may have multiple QT's
|
||||
while qt_length > 0 {
|
||||
let qt_info = img.stream.read_u8_err()?;
|
||||
// 0 = 8 bit otherwise 16 bit dqt
|
||||
let precision = (qt_info >> 4) as usize;
|
||||
// last 4 bits give us position
|
||||
let table_position = (qt_info & 0x0f) as usize;
|
||||
let precision_value = 64 * (precision + 1);
|
||||
|
||||
if (precision_value + 1) as u16 > qt_length {
|
||||
return Err(DecodeErrors::DqtError(format!("Invalid QT table bytes left :{}. Too small to construct a valid qt table which should be {} long", qt_length, precision_value + 1)));
|
||||
}
|
||||
|
||||
let dct_table = match precision {
|
||||
0 => {
|
||||
let mut qt_values = [0; 64];
|
||||
|
||||
img.stream.read_exact_bytes(&mut qt_values)?;
|
||||
|
||||
qt_length -= (precision_value as u16) + 1 /*QT BIT*/;
|
||||
// carry out un zig-zag here
|
||||
un_zig_zag(&qt_values)
|
||||
}
|
||||
1 => {
|
||||
// 16 bit quantization tables
|
||||
let mut qt_values = [0_u16; 64];
|
||||
|
||||
for i in 0..64 {
|
||||
qt_values[i] = img.stream.get_u16_be_err()?;
|
||||
}
|
||||
qt_length -= (precision_value as u16) + 1;
|
||||
|
||||
un_zig_zag(&qt_values)
|
||||
}
|
||||
_ => {
|
||||
return Err(DecodeErrors::DqtError(format!(
|
||||
"Expected QT precision value of either 0 or 1, found {precision:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if table_position >= MAX_COMPONENTS {
|
||||
return Err(DecodeErrors::DqtError(format!(
|
||||
"Too large table position for QT :{table_position}, expected between 0 and 3"
|
||||
)));
|
||||
}
|
||||
|
||||
trace!("Assigning qt table {table_position} with precision {precision}");
|
||||
img.qt_tables[table_position] = Some(dct_table);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Section:`B.2.2 Frame header syntax`
|
||||
|
||||
pub(crate) fn parse_start_of_frame<T: ZByteReaderTrait>(
|
||||
sof: SOFMarkers, img: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
if img.seen_sof {
|
||||
return Err(DecodeErrors::SofError(
|
||||
"Two Start of Frame Markers".to_string()
|
||||
));
|
||||
}
|
||||
// Get length of the frame header
|
||||
let length = img.stream.get_u16_be_err()?;
|
||||
// usually 8, but can be 12 and 16, we currently support only 8
|
||||
// so sorry about that 12 bit images
|
||||
let dt_precision = img.stream.read_u8_err()?;
|
||||
|
||||
if dt_precision != 8 {
|
||||
return Err(DecodeErrors::SofError(format!(
|
||||
"The library can only parse 8-bit images, the image has {dt_precision} bits of precision"
|
||||
)));
|
||||
}
|
||||
|
||||
img.info.set_density(dt_precision);
|
||||
|
||||
// read and set the image height.
|
||||
let img_height = img.stream.get_u16_be_err()?;
|
||||
img.info.set_height(img_height);
|
||||
|
||||
// read and set the image width
|
||||
let img_width = img.stream.get_u16_be_err()?;
|
||||
img.info.set_width(img_width);
|
||||
|
||||
trace!("Image width :{}", img_width);
|
||||
trace!("Image height :{}", img_height);
|
||||
|
||||
if usize::from(img_width) > img.options.max_width() {
|
||||
return Err(DecodeErrors::Format(format!("Image width {} greater than width limit {}. If use `set_limits` if you want to support huge images", img_width, img.options.max_width())));
|
||||
}
|
||||
|
||||
if usize::from(img_height) > img.options.max_height() {
|
||||
return Err(DecodeErrors::Format(format!("Image height {} greater than height limit {}. If use `set_limits` if you want to support huge images", img_height, img.options.max_height())));
|
||||
}
|
||||
|
||||
// Check image width or height is zero
|
||||
if img_width == 0 || img_height == 0 {
|
||||
return Err(DecodeErrors::ZeroError);
|
||||
}
|
||||
|
||||
// Number of components for the image.
|
||||
let num_components = img.stream.read_u8_err()?;
|
||||
|
||||
if num_components == 0 {
|
||||
return Err(DecodeErrors::SofError(
|
||||
"Number of components cannot be zero.".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
let expected = 8 + 3 * u16::from(num_components);
|
||||
// length should be equal to num components
|
||||
if length != expected {
|
||||
return Err(DecodeErrors::SofError(format!(
|
||||
"Length of start of frame differs from expected {expected},value is {length}"
|
||||
)));
|
||||
}
|
||||
|
||||
trace!("Image components : {}", num_components);
|
||||
|
||||
if num_components == 1 {
|
||||
// SOF sets the number of image components
|
||||
// and that to us translates to setting input and output
|
||||
// colorspaces to zero
|
||||
img.input_colorspace = ColorSpace::Luma;
|
||||
//img.options = img.options.jpeg_set_out_colorspace(ColorSpace::Luma);
|
||||
debug!("Overriding default colorspace set to Luma");
|
||||
}
|
||||
if num_components == 4 && img.input_colorspace == ColorSpace::YCbCr {
|
||||
trace!("Input image has 4 components, defaulting to CMYK colorspace");
|
||||
// https://entropymine.wordpress.com/2018/10/22/how-is-a-jpeg-images-color-type-determined/
|
||||
img.input_colorspace = ColorSpace::CMYK;
|
||||
}
|
||||
|
||||
// set number of components
|
||||
img.info.components = num_components;
|
||||
|
||||
let mut components = Vec::with_capacity(num_components as usize);
|
||||
let mut temp = [0; 3];
|
||||
|
||||
for pos in 0..num_components {
|
||||
// read 3 bytes for each component
|
||||
img.stream.read_exact_bytes(&mut temp)?;
|
||||
|
||||
// create a component.
|
||||
let component = Components::from(temp, pos)?;
|
||||
|
||||
components.push(component);
|
||||
}
|
||||
img.seen_sof = true;
|
||||
|
||||
img.info.set_sof_marker(sof);
|
||||
|
||||
img.components = components;
|
||||
|
||||
let mut h_max = 1;
|
||||
let mut v_max = 1;
|
||||
|
||||
for comp in &img.components {
|
||||
h_max = max(h_max, comp.horizontal_sample);
|
||||
v_max = max(v_max, comp.vertical_sample);
|
||||
}
|
||||
|
||||
img.info.sample_ratio = match (h_max, v_max) {
|
||||
(1, 1) => SampleRatios::None,
|
||||
(1, 2) => SampleRatios::V,
|
||||
(2, 1) => SampleRatios::H,
|
||||
(2, 2) => SampleRatios::HV,
|
||||
(hs, vs) => SampleRatios::Generic(hs, vs)
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a start of scan data
|
||||
pub(crate) fn parse_sos<T: ZByteReaderTrait>(
|
||||
image: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
// Scan header length
|
||||
let ls = usize::from(image.stream.get_u16_be_err()?);
|
||||
// Number of image components in scan
|
||||
let ns = image.stream.read_u8_err()?;
|
||||
|
||||
let mut seen: [_; 5] = [-1; { MAX_COMPONENTS + 1 }];
|
||||
|
||||
image.num_scans = ns;
|
||||
let smallest_size = 6 + 2 * usize::from(ns);
|
||||
|
||||
if ls != smallest_size {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Bad SOS length {ls},corrupt jpeg"
|
||||
)));
|
||||
}
|
||||
|
||||
// Check number of components.
|
||||
if !(1..5).contains(&ns) {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Invalid number of components in start of scan {ns}, expected in range 1..5"
|
||||
)));
|
||||
}
|
||||
|
||||
if image.info.components == 0 {
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Error decoding SOF Marker, Number of components cannot be zero."
|
||||
));
|
||||
}
|
||||
|
||||
// consume spec parameters
|
||||
image.scan_subsampled = false;
|
||||
|
||||
for i in 0..ns {
|
||||
let id = image.stream.read_u8_err()?;
|
||||
|
||||
if seen.contains(&i32::from(id)) {
|
||||
return Err(DecodeErrors::SofError(format!(
|
||||
"Duplicate ID {id} seen twice in the same component"
|
||||
)));
|
||||
}
|
||||
|
||||
seen[usize::from(i)] = i32::from(id);
|
||||
// DC and AC huffman table position
|
||||
// top 4 bits contain dc huffman destination table
|
||||
// lower four bits contain ac huffman destination table
|
||||
let y = image.stream.read_u8_err()?;
|
||||
|
||||
let mut j = 0;
|
||||
|
||||
while j < image.info.components {
|
||||
if image.components[j as usize].id == id {
|
||||
break;
|
||||
}
|
||||
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if j == image.info.components {
|
||||
return Err(DecodeErrors::SofError(format!(
|
||||
"Invalid component id {}, expected one one of {:?}",
|
||||
id,
|
||||
image.components.iter().map(|c| c.id).collect::<Vec<_>>()
|
||||
)));
|
||||
}
|
||||
|
||||
let component = &mut image.components[usize::from(j)];
|
||||
component.dc_huff_table = usize::from((y >> 4) & 0xF);
|
||||
component.ac_huff_table = usize::from(y & 0xF);
|
||||
image.z_order[i as usize] = j as usize;
|
||||
|
||||
if component.vertical_sample != 1 || component.horizontal_sample != 1 {
|
||||
image.scan_subsampled = true;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Assigned huffman tables {}/{} to component {j}, id={}",
|
||||
image.components[usize::from(j)].dc_huff_table,
|
||||
image.components[usize::from(j)].ac_huff_table,
|
||||
image.components[usize::from(j)].id,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect the component spec parameters
|
||||
// This is only needed for progressive images but I'll read
|
||||
// them in order to ensure they are correct according to the spec
|
||||
|
||||
// Extract progressive information
|
||||
|
||||
// https://www.w3.org/Graphics/JPEG/itu-t81.pdf
|
||||
// Page 42
|
||||
|
||||
// Start of spectral / predictor selection. (between 0 and 63)
|
||||
image.spec_start = image.stream.read_u8_err()?;
|
||||
// End of spectral selection
|
||||
image.spec_end = image.stream.read_u8_err()?;
|
||||
|
||||
let bit_approx = image.stream.read_u8_err()?;
|
||||
// successive approximation bit position high
|
||||
image.succ_high = bit_approx >> 4;
|
||||
|
||||
if image.spec_end > 63 {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Invalid Se parameter {}, range should be 0-63",
|
||||
image.spec_end
|
||||
)));
|
||||
}
|
||||
if image.spec_start > 63 {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Invalid Ss parameter {}, range should be 0-63",
|
||||
image.spec_start
|
||||
)));
|
||||
}
|
||||
if image.succ_high > 13 {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Invalid Ah parameter {}, range should be 0-13",
|
||||
image.succ_low
|
||||
)));
|
||||
}
|
||||
// successive approximation bit position low
|
||||
image.succ_low = bit_approx & 0xF;
|
||||
|
||||
if image.succ_low > 13 {
|
||||
return Err(DecodeErrors::SosError(format!(
|
||||
"Invalid Al parameter {}, range should be 0-13",
|
||||
image.succ_low
|
||||
)));
|
||||
}
|
||||
// skip any bytes not read
|
||||
image.stream.skip(smallest_size.saturating_sub(ls))?;
|
||||
|
||||
trace!(
|
||||
"Ss={}, Se={} Ah={} Al={}",
|
||||
image.spec_start,
|
||||
image.spec_end,
|
||||
image.succ_high,
|
||||
image.succ_low
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the APP13 (IPTC) segment.
|
||||
pub(crate) fn parse_app13<T: ZByteReaderTrait>(
|
||||
decoder: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
const IPTC_PREFIX: &[u8] = b"Photoshop 3.0\0";
|
||||
// skip length.
|
||||
let mut length = usize::from(decoder.stream.get_u16_be());
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::FormatStatic("Too small APP13 length"));
|
||||
}
|
||||
// length bytes.
|
||||
length -= 2;
|
||||
|
||||
if length > IPTC_PREFIX.len() && decoder.stream.peek_at(0, IPTC_PREFIX.len())? == IPTC_PREFIX {
|
||||
// skip bytes we read above.
|
||||
decoder.stream.skip(IPTC_PREFIX.len())?;
|
||||
length -= IPTC_PREFIX.len();
|
||||
|
||||
let iptc_bytes = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
|
||||
decoder.info.iptc_data = Some(iptc_bytes);
|
||||
}
|
||||
|
||||
decoder.stream.skip(length)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse Adobe App14 segment
|
||||
pub(crate) fn parse_app14<T: ZByteReaderTrait>(
|
||||
decoder: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
// skip length
|
||||
let mut length = usize::from(decoder.stream.get_u16_be());
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::FormatStatic("Too small APP14 length"));
|
||||
}
|
||||
|
||||
if decoder.stream.peek_at(0, 5)? == b"Adobe" {
|
||||
if length < 14 {
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Too short of a length for App14 segment"
|
||||
));
|
||||
}
|
||||
// move stream 6 bytes to remove adobe id
|
||||
decoder.stream.skip(6)?;
|
||||
// skip version, flags0 and flags1
|
||||
decoder.stream.skip(5)?;
|
||||
// get color transform
|
||||
let transform = decoder.stream.read_u8();
|
||||
// https://exiftool.org/TagNames/JPEG.html#Adobe
|
||||
match transform {
|
||||
0 => decoder.input_colorspace = ColorSpace::CMYK,
|
||||
1 => decoder.input_colorspace = ColorSpace::YCbCr,
|
||||
2 => decoder.input_colorspace = ColorSpace::YCCK,
|
||||
_ => {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Unknown Adobe colorspace {transform}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
// length = 2
|
||||
// adobe id = 6
|
||||
// version = 5
|
||||
// transform = 1
|
||||
length = length.saturating_sub(14);
|
||||
} else {
|
||||
warn!("Not a valid Adobe APP14 Segment, skipping {} bytes", length);
|
||||
length = length.saturating_sub(2);
|
||||
}
|
||||
// skip any proceeding lengths.
|
||||
// we do not need them
|
||||
decoder.stream.skip(length)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the APP1 segment
|
||||
///
|
||||
/// This contains the exif tag
|
||||
pub(crate) fn parse_app1<T: ZByteReaderTrait>(
|
||||
decoder: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
const XMP_NAMESPACE_PREFIX: &[u8] = b"http://ns.adobe.com/xap/1.0/\0";
|
||||
|
||||
// contains exif data
|
||||
let mut length = usize::from(decoder.stream.get_u16_be());
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::FormatStatic("Too small app1 length"));
|
||||
}
|
||||
// length bytes
|
||||
length -= 2;
|
||||
|
||||
if length > 6 && decoder.stream.peek_at(0, 6)? == b"Exif\x00\x00" {
|
||||
trace!("Exif segment present");
|
||||
// skip bytes we read above
|
||||
decoder.stream.skip(6)?;
|
||||
length -= 6;
|
||||
|
||||
let exif_bytes = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
|
||||
decoder.info.exif_data = Some(exif_bytes);
|
||||
} else if length > XMP_NAMESPACE_PREFIX.len()
|
||||
&& decoder.stream.peek_at(0, XMP_NAMESPACE_PREFIX.len())? == XMP_NAMESPACE_PREFIX
|
||||
{
|
||||
trace!("XMP Data Present");
|
||||
decoder.stream.skip(XMP_NAMESPACE_PREFIX.len())?;
|
||||
length -= XMP_NAMESPACE_PREFIX.len();
|
||||
let xmp_data = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
decoder.info.xmp_data = Some(xmp_data);
|
||||
} else {
|
||||
warn!("Unknown format for APP1 tag, skipping");
|
||||
}
|
||||
|
||||
decoder.stream.skip(length)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_app2<T: ZByteReaderTrait>(
|
||||
decoder: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
static HDR_META: &[u8] = b"urn:iso:std:iso:ts:21496:-1\0";
|
||||
static MPF_DATA: &[u8] = b"MPF\0";
|
||||
|
||||
let mut length = usize::from(decoder.stream.get_u16_be());
|
||||
|
||||
if length < 2 {
|
||||
return Err(DecodeErrors::FormatStatic("Too small app2 segment"));
|
||||
}
|
||||
// length bytes
|
||||
length -= 2;
|
||||
|
||||
if length > 14 && decoder.stream.peek_at(0, 12)? == *b"ICC_PROFILE\0" {
|
||||
trace!("ICC Profile present");
|
||||
// skip 12 bytes which indicate ICC profile
|
||||
length -= 12;
|
||||
decoder.stream.skip(12)?;
|
||||
let seq_no = decoder.stream.read_u8();
|
||||
let num_markers = decoder.stream.read_u8();
|
||||
// deduct the two bytes we read above
|
||||
length -= 2;
|
||||
|
||||
let data = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
|
||||
let icc_chunk = ICCChunk {
|
||||
seq_no,
|
||||
num_markers,
|
||||
data
|
||||
};
|
||||
decoder.icc_data.push(icc_chunk);
|
||||
} else if length > HDR_META.len() && decoder.stream.peek_at(0, HDR_META.len())? == HDR_META {
|
||||
length = length.saturating_sub(HDR_META.len());
|
||||
decoder.stream.skip(HDR_META.len())?;
|
||||
trace!("Gain Map metadata found");
|
||||
match length {
|
||||
4 => {
|
||||
// If gain map metadata length == 4 then here it variables
|
||||
// https://github.com/google/libultrahdr/blob/bf2aa439eea9ad5da483003fa44182f990f74091/lib/src/jpegr.cpp#L1076C1-L1077C35
|
||||
// 2 bytes minimum_version: (00 00)
|
||||
// 2 bytes writer_version: (00 00)
|
||||
// Perhaps nothing to do with it ?
|
||||
let _ = decoder.stream.get_u16_be();
|
||||
let _ = decoder.stream.get_u16_be();
|
||||
length -= 4;
|
||||
decoder
|
||||
.info
|
||||
.gain_map_info
|
||||
.push(GainMapInfo { data: Vec::new() });
|
||||
}
|
||||
n if n > 4 => {
|
||||
// If there is perhaps useful gain map info
|
||||
// we'll read this until end
|
||||
// https://github.com/google/libultrahdr/blob/bf2aa439eea9ad5da483003fa44182f990f74091/lib/src/jpegr.cpp#L1323
|
||||
let data = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
length -= data.len();
|
||||
decoder.stream.skip(data.len())?;
|
||||
|
||||
decoder.info.gain_map_info.push(GainMapInfo { data });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if length > MPF_DATA.len() && decoder.stream.peek_at(0, MPF_DATA.len())? == MPF_DATA {
|
||||
trace!("MPF Signature present");
|
||||
length = length.saturating_sub(MPF_DATA.len());
|
||||
decoder.stream.skip(MPF_DATA.len())?;
|
||||
// MPF signature taken from here
|
||||
// https://github.com/google/libultrahdr/blob/bf2aa439eea9ad5da483003fa44182f990f74091/lib/include/ultrahdr/multipictureformat.h#L50
|
||||
// https://github.com/google/libultrahdr/blob/bf2aa439eea9ad5da483003fa44182f990f74091/lib/src/multipictureformat.cpp#L36
|
||||
// More info https://www.cipa.jp/std/documents/e/DC-X007-KEY_E.pdf
|
||||
let data = decoder.stream.peek_at(0, length)?.to_vec();
|
||||
length -= data.len();
|
||||
decoder.stream.skip(data.len())?;
|
||||
decoder.info.multi_picture_information = Some(data);
|
||||
}
|
||||
|
||||
decoder.stream.skip(length)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Small utility function to print Un-zig-zagged quantization tables
|
||||
|
||||
fn un_zig_zag<T>(a: &[T]) -> [i32; 64]
|
||||
where
|
||||
T: Default + Copy,
|
||||
i32: core::convert::From<T>
|
||||
{
|
||||
let mut output = [i32::default(); 64];
|
||||
|
||||
for i in 0..64 {
|
||||
output[UN_ZIGZAG[i]] = i32::from(a[i]);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
Vendored
+254
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! This file contains a single struct `HuffmanTable` that
|
||||
//! stores Huffman tables needed during `BitStream` decoding.
|
||||
#![allow(clippy::similar_names, clippy::module_name_repetitions)]
|
||||
|
||||
use alloc::string::ToString;
|
||||
|
||||
use crate::errors::DecodeErrors;
|
||||
|
||||
/// Determines how many bits of lookahead we have for our bitstream decoder.
|
||||
|
||||
pub const HUFF_LOOKAHEAD: u8 = 9;
|
||||
|
||||
/// A struct which contains necessary tables for decoding a JPEG
|
||||
/// huffman encoded bitstream
|
||||
|
||||
pub struct HuffmanTable {
|
||||
// element `[0]` of each array is unused
|
||||
/// largest code of length k
|
||||
pub(crate) maxcode: [i32; 18],
|
||||
/// offset for codes of length k
|
||||
/// Answers the question, where do code-lengths of length k end
|
||||
/// Element 0 is unused
|
||||
pub(crate) offset: [i32; 18],
|
||||
/// lookup table for fast decoding
|
||||
///
|
||||
/// top bits above HUFF_LOOKAHEAD contain the code length.
|
||||
///
|
||||
/// Lower (8) bits contain the symbol in order of increasing code length.
|
||||
pub(crate) lookup: [i32; 1 << HUFF_LOOKAHEAD],
|
||||
|
||||
/// A table which can be used to decode small AC coefficients and
|
||||
/// do an equivalent of receive_extend
|
||||
pub(crate) ac_lookup: Option<[i16; 1 << HUFF_LOOKAHEAD]>,
|
||||
|
||||
/// Directly represent contents of a JPEG DHT marker
|
||||
///
|
||||
/// \# number of symbols with codes of length `k` bits
|
||||
// bits[0] is unused
|
||||
/// Symbols in order of increasing code length
|
||||
pub(crate) values: [u8; 256]
|
||||
}
|
||||
|
||||
impl HuffmanTable {
|
||||
pub fn new(
|
||||
codes: &[u8; 17], values: [u8; 256], is_dc: bool, is_progressive: bool
|
||||
) -> Result<HuffmanTable, DecodeErrors> {
|
||||
let too_long_code = (i32::from(HUFF_LOOKAHEAD) + 1) << HUFF_LOOKAHEAD;
|
||||
let mut p = HuffmanTable {
|
||||
maxcode: [0; 18],
|
||||
offset: [0; 18],
|
||||
lookup: [too_long_code; 1 << HUFF_LOOKAHEAD],
|
||||
values,
|
||||
ac_lookup: None
|
||||
};
|
||||
|
||||
p.make_derived_table(is_dc, is_progressive, codes)?;
|
||||
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Create a new huffman tables with values that aren't fixed
|
||||
/// used by fill_mjpeg_tables
|
||||
pub fn new_unfilled(
|
||||
codes: &[u8; 17], values: &[u8], is_dc: bool, is_progressive: bool
|
||||
) -> Result<HuffmanTable, DecodeErrors> {
|
||||
let mut buf = [0; 256];
|
||||
buf[..values.len()].copy_from_slice(values);
|
||||
HuffmanTable::new(codes, buf, is_dc, is_progressive)
|
||||
}
|
||||
|
||||
/// Compute derived values for a Huffman table
|
||||
///
|
||||
/// This routine performs some validation checks on the table
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::too_many_lines,
|
||||
clippy::needless_range_loop
|
||||
)]
|
||||
fn make_derived_table(
|
||||
&mut self, is_dc: bool, _is_progressive: bool, bits: &[u8; 17]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
// build a list of code size
|
||||
let mut huff_size = [0; 257];
|
||||
// Huffman code lengths
|
||||
let mut huff_code: [u32; 257] = [0; 257];
|
||||
// figure C.1 make table of Huffman code length for each symbol
|
||||
let mut p = 0;
|
||||
|
||||
for l in 1..=16 {
|
||||
let mut i = i32::from(bits[l]);
|
||||
// table overrun is checked before ,so we dont need to check
|
||||
while i != 0 {
|
||||
huff_size[p] = l as u8;
|
||||
p += 1;
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
huff_size[p] = 0;
|
||||
|
||||
let num_symbols = p;
|
||||
// Generate the codes themselves
|
||||
// We also validate that the counts represent a legal Huffman code tree
|
||||
let mut code = 0;
|
||||
let mut si = i32::from(huff_size[0]);
|
||||
|
||||
p = 0;
|
||||
|
||||
while huff_size[p] != 0 {
|
||||
while i32::from(huff_size[p]) == si {
|
||||
huff_code[p] = code;
|
||||
code += 1;
|
||||
p += 1;
|
||||
}
|
||||
// maximum code of length si, pre-shifted by 16-k bits
|
||||
self.maxcode[si as usize] = (code << (16 - si)) as i32;
|
||||
// code is now 1 more than the last code used for code-length si; but
|
||||
// it must still fit in si bits, since no code is allowed to be all ones.
|
||||
if (code as i32) >= (1 << si) {
|
||||
return Err(DecodeErrors::HuffmanDecode("Bad Huffman Table".to_string()));
|
||||
}
|
||||
|
||||
code <<= 1;
|
||||
si += 1;
|
||||
}
|
||||
|
||||
// Figure F.15 generate decoding tables for bit-sequential decoding
|
||||
p = 0;
|
||||
|
||||
for l in 0..=16 {
|
||||
if bits[l] == 0 {
|
||||
// -1 if no codes of this length
|
||||
self.maxcode[l] = -1;
|
||||
} else {
|
||||
// offset[l]=codes[index of 1st symbol of code length l
|
||||
// minus minimum code of length l]
|
||||
self.offset[l] = (p as i32) - (huff_code[p]) as i32;
|
||||
p += usize::from(bits[l]);
|
||||
}
|
||||
}
|
||||
|
||||
self.offset[17] = 0;
|
||||
// we ensure that decode terminates
|
||||
self.maxcode[17] = 0x000F_FFFF;
|
||||
|
||||
/*
|
||||
* Compute lookahead tables to speed up decoding.
|
||||
* First we set all the table entries to 0(left justified), indicating "too long";
|
||||
* (Note too long was set during initialization)
|
||||
* then we iterate through the Huffman codes that are short enough and
|
||||
* fill in all the entries that correspond to bit sequences starting
|
||||
* with that code.
|
||||
*/
|
||||
|
||||
p = 0;
|
||||
|
||||
for l in 1..=HUFF_LOOKAHEAD {
|
||||
for _ in 1..=i32::from(bits[usize::from(l)]) {
|
||||
// l -> Current code length,
|
||||
// p => Its index in self.code and self.values
|
||||
// Generate left justified code followed by all possible bit sequences
|
||||
let mut look_bits = (huff_code[p] as usize) << (HUFF_LOOKAHEAD - l);
|
||||
|
||||
for _ in 0..1 << (HUFF_LOOKAHEAD - l) {
|
||||
self.lookup[look_bits] =
|
||||
(i32::from(l) << HUFF_LOOKAHEAD) | i32::from(self.values[p]);
|
||||
look_bits += 1;
|
||||
}
|
||||
|
||||
p += 1;
|
||||
}
|
||||
}
|
||||
// build an ac table that does an equivalent of decode and receive_extend
|
||||
if !is_dc {
|
||||
let mut fast = [255; 1 << HUFF_LOOKAHEAD];
|
||||
// Iterate over number of symbols
|
||||
for i in 0..num_symbols {
|
||||
// get code size for an item
|
||||
let s = huff_size[i];
|
||||
|
||||
if s <= HUFF_LOOKAHEAD {
|
||||
// if it's lower than what we need for our lookup table create the table
|
||||
let c = (huff_code[i] << (HUFF_LOOKAHEAD - s)) as usize;
|
||||
let m = (1 << (HUFF_LOOKAHEAD - s)) as usize;
|
||||
|
||||
for j in 0..m {
|
||||
fast[c + j] = i as i16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build a table that decodes both magnitude and value of small ACs in
|
||||
// one go.
|
||||
let mut fast_ac = [0; 1 << HUFF_LOOKAHEAD];
|
||||
|
||||
for i in 0..(1 << HUFF_LOOKAHEAD) {
|
||||
let fast_v = fast[i];
|
||||
|
||||
if fast_v < 255 {
|
||||
// get symbol value from AC table
|
||||
let rs = self.values[fast_v as usize];
|
||||
// shift by 4 to get run length
|
||||
let run = i16::from((rs >> 4) & 15);
|
||||
// get magnitude bits stored at the lower 3 bits
|
||||
let mag_bits = i16::from(rs & 15);
|
||||
// length of the bit we've read
|
||||
let len = i16::from(huff_size[fast_v as usize]);
|
||||
|
||||
if mag_bits != 0 && (len + mag_bits) <= i16::from(HUFF_LOOKAHEAD) {
|
||||
// magnitude code followed by receive_extend code
|
||||
let mut k = (((i as i16) << len) & ((1 << HUFF_LOOKAHEAD) - 1))
|
||||
>> (i16::from(HUFF_LOOKAHEAD) - mag_bits);
|
||||
let m = 1 << (mag_bits - 1);
|
||||
|
||||
if k < m {
|
||||
k += (!0_i16 << mag_bits) + 1;
|
||||
};
|
||||
|
||||
// if result is small enough fit into fast ac table
|
||||
if (-128..=127).contains(&k) {
|
||||
fast_ac[i] = (k << 8) + (run << 4) + (len + mag_bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ac_lookup = Some(fast_ac);
|
||||
}
|
||||
|
||||
// Validate symbols as being reasonable
|
||||
// For AC tables, we make no check, but accept all byte values 0..255
|
||||
// For DC tables, we require symbols to be in range 0..15
|
||||
if is_dc {
|
||||
for i in 0..num_symbols {
|
||||
let sym = self.values[i];
|
||||
|
||||
if sym > 15 {
|
||||
return Err(DecodeErrors::HuffmanDecode("Bad Huffman Table".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+206
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Routines for IDCT
|
||||
//!
|
||||
//! Essentially we provide 2 routines for IDCT, a scalar implementation and a not super optimized
|
||||
//! AVX2 one, i'll talk about them here.
|
||||
//!
|
||||
//! There are 2 reasons why we have the avx one
|
||||
//! 1. No one compiles with -C target-features=avx2 hence binaries won't probably take advantage(even
|
||||
//! if it exists).
|
||||
//! 2. AVX employs zero short circuit in a way the scalar code cannot employ it.
|
||||
//! - AVX does this by checking for MCU's whose 63 AC coefficients are zero and if true, it writes
|
||||
//! values directly, if false, it goes the long way of calculating.
|
||||
//! - Although this can be trivially implemented in the scalar version, it generates code
|
||||
//! I'm not happy width(scalar version that basically loops and that is too many branches for me)
|
||||
//! The avx one does a better job of using bitwise or's with (`_mm256_or_si256`) which is magnitudes of faster
|
||||
//! than anything I could come up with
|
||||
//!
|
||||
//! The AVX code also has some cool transpose_u16 instructions which look so complicated to be cool
|
||||
//! (spoiler alert, i barely understand how it works, that's why I credited the owner).
|
||||
//!
|
||||
#![allow(
|
||||
clippy::excessive_precision,
|
||||
clippy::unreadable_literal,
|
||||
clippy::module_name_repetitions,
|
||||
unused_parens,
|
||||
clippy::wildcard_imports
|
||||
)]
|
||||
|
||||
use zune_core::log::debug;
|
||||
use zune_core::options::DecoderOptions;
|
||||
|
||||
use crate::decoder::IDCTPtr;
|
||||
use crate::idct::scalar::{idct_int, idct_int_1x1};
|
||||
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod avx2;
|
||||
#[cfg(feature = "neon")]
|
||||
pub mod neon;
|
||||
|
||||
pub mod scalar;
|
||||
|
||||
/// Choose an appropriate IDCT function
|
||||
#[allow(unused_variables)]
|
||||
pub fn choose_idct_func(options: &DecoderOptions) -> IDCTPtr {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
{
|
||||
if options.use_avx2() {
|
||||
debug!("Using vector integer IDCT");
|
||||
return |a: &mut [i32; 64], b: &mut [i16], c: usize| {
|
||||
// SAFETY: `options.use_avx2()` only returns true if avx2 is supported.
|
||||
unsafe { avx2::idct_avx2(a,b,c) }
|
||||
};
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
{
|
||||
if options.use_neon() {
|
||||
debug!("Using vector integer IDCT");
|
||||
return |a: &mut [i32; 64], b: &mut [i16], c: usize| {
|
||||
// SAFETY: `options.use_neon()` only returns true if neon is supported.
|
||||
unsafe { neon::idct_neon(a,b,c) }
|
||||
};
|
||||
}
|
||||
}
|
||||
debug!("Using scalar integer IDCT");
|
||||
// use generic one
|
||||
return idct_int;
|
||||
}
|
||||
|
||||
/// Choose a function to implement 4x4 IDCT.
|
||||
///
|
||||
/// These functions get the same input but have an extra contract: Only the first 4x4 block of
|
||||
/// coefficients are non-zero. All other entries are zeroed.
|
||||
///
|
||||
/// **The callee must uphold that contract on return**
|
||||
pub fn choose_idct_4x4_func(_options: &DecoderOptions) -> IDCTPtr {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
{
|
||||
if _options.use_avx2() {
|
||||
debug!("Using vector integer IDCT");
|
||||
return |a: &mut [i32; 64], b: &mut [i16], c: usize| {
|
||||
// SAFETY: `options.use_avx2()` only returns true if avx2 is supported.
|
||||
unsafe { avx2::idct_avx2_4x4(a,b,c) }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
scalar::idct4x4
|
||||
}
|
||||
|
||||
pub fn choose_idct_1x1_func(_: &DecoderOptions) -> IDCTPtr {
|
||||
// These are simple stores, no alternative implementation for now
|
||||
idct_int_1x1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unreachable_code)]
|
||||
#[allow(dead_code)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn idct_test0() {
|
||||
let stride = 8;
|
||||
let mut coeff = [10; 64];
|
||||
let mut coeff2 = [10; 64];
|
||||
let mut output_scalar = [0; 64];
|
||||
let mut output_vector = [0; 64];
|
||||
let idct_func = choose_idct_func(&DecoderOptions::new_fast());
|
||||
idct_func(&mut coeff, &mut output_vector, stride);
|
||||
idct_int(&mut coeff2, &mut output_scalar, stride);
|
||||
assert_eq!(output_scalar, output_vector, "IDCT and scalar do not match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_idct_test1() {
|
||||
let stride = 8;
|
||||
let mut coeff = [14; 64];
|
||||
let mut coeff2 = [14; 64];
|
||||
let mut output_scalar = [0; 64];
|
||||
let mut output_vector = [0; 64];
|
||||
let idct_func = choose_idct_func(&DecoderOptions::new_fast());
|
||||
idct_func(&mut coeff, &mut output_vector, stride);
|
||||
idct_int(&mut coeff2, &mut output_scalar, stride);
|
||||
assert_eq!(output_scalar, output_vector, "IDCT and scalar do not match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_idct_test2() {
|
||||
let stride = 8;
|
||||
let mut coeff = [0; 64];
|
||||
coeff[0] = 255;
|
||||
coeff[63] = -256;
|
||||
let mut coeff2 = coeff;
|
||||
let mut output_scalar = [0; 64];
|
||||
let mut output_vector = [0; 64];
|
||||
let idct_func = choose_idct_func(&DecoderOptions::new_fast());
|
||||
idct_func(&mut coeff, &mut output_vector, stride);
|
||||
idct_int(&mut coeff2, &mut output_scalar, stride);
|
||||
assert_eq!(output_scalar, output_vector, "IDCT and scalar do not match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_idct_zeros() {
|
||||
let stride = 8;
|
||||
let mut coeff = [0; 64];
|
||||
let mut coeff2 = [0; 64];
|
||||
let mut output_scalar = [0; 64];
|
||||
let mut output_vector = [0; 64];
|
||||
let idct_func = choose_idct_func(&DecoderOptions::new_fast());
|
||||
idct_func(&mut coeff, &mut output_vector, stride);
|
||||
idct_int(&mut coeff2, &mut output_scalar, stride);
|
||||
assert_eq!(output_scalar, output_vector, "IDCT and scalar do not match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idct_4x4() {
|
||||
#[rustfmt::skip]
|
||||
const A: [i32; 32] = [
|
||||
-254, -7, 0, 0, 0, 0, 0, 0,
|
||||
7, 0, -30, 32, 0, 0, 0, 0,
|
||||
7, 0, -30, 32, 0, 0, 0, 0,
|
||||
7, 0, -30, 32, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
let v: Vec<IDCTPtr> = vec![
|
||||
choose_idct_func(&DecoderOptions::new_safe()),
|
||||
choose_idct_4x4_func(&DecoderOptions::new_safe()),
|
||||
choose_idct_func(&DecoderOptions::new_fast()),
|
||||
choose_idct_4x4_func(&DecoderOptions::new_fast()),
|
||||
];
|
||||
let dct_names = vec![
|
||||
"safe idct",
|
||||
"safe idct 4x4",
|
||||
"fast idct",
|
||||
"fast idct 4x4",
|
||||
];
|
||||
|
||||
let mut color = vec![];
|
||||
|
||||
for idct in v {
|
||||
let mut a = [0i32; 64];
|
||||
a[..32].copy_from_slice(&A);
|
||||
let mut b = [0i16; 64];
|
||||
|
||||
idct(&mut a, &mut b, 8);
|
||||
|
||||
color.push(b);
|
||||
}
|
||||
|
||||
for (wnd, name) in color.windows(2).zip(&dct_names) {
|
||||
let [a, b] = wnd else { unreachable!() };
|
||||
assert_eq!(a, b, "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
//! AVX optimised IDCT.
|
||||
//!
|
||||
//! Okay not thaat optimised.
|
||||
//!
|
||||
//!
|
||||
//! # The implementation
|
||||
//! The implementation is neatly broken down into two operations.
|
||||
//!
|
||||
//! 1. Test for zeroes
|
||||
//! > There is a shortcut method for idct where when all AC values are zero, we can get the answer really quickly.
|
||||
//! by scaling the 1/8th of the DCT coefficient of the block to the whole block and level shifting.
|
||||
//!
|
||||
//! 2. If above fails, we proceed to carry out IDCT as a two pass one dimensional algorithm.
|
||||
//! IT does two whole scans where it carries out IDCT on all items
|
||||
//! After each successive scan, data is transposed in register(thank you x86 SIMD powers). and the second
|
||||
//! pass is carried out.
|
||||
//!
|
||||
//! The code is not super optimized, it produces bit identical results with scalar code hence it's
|
||||
//! `mm256_add_epi16`
|
||||
//! and it also has the advantage of making this implementation easy to maintain.
|
||||
|
||||
#![cfg(feature = "x86")]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
use core::arch::x86::*;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use core::arch::x86_64::*;
|
||||
|
||||
use crate::unsafe_utils::{transpose, YmmRegister};
|
||||
|
||||
const SCALE_BITS: i32 = 512 + 65536 + (128 << 17);
|
||||
|
||||
// Pack i32 to i16's,
|
||||
// clamp them to be between 0-255
|
||||
// Undo shuffling
|
||||
// Store back to array
|
||||
macro_rules! permute_store {
|
||||
($x:tt,$y:tt,$index:tt,$out:tt,$stride:tt) => {
|
||||
let a = _mm256_packs_epi32($x, $y);
|
||||
|
||||
// Clamp the values after packing, we can clamp more values at once
|
||||
let b = clamp_avx(a);
|
||||
|
||||
// /Undo shuffling
|
||||
let c = _mm256_permute4x64_epi64(b, shuffle(3, 1, 2, 0));
|
||||
|
||||
// store first vector
|
||||
_mm_storeu_si128(
|
||||
($out)
|
||||
.get_mut($index..$index + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
_mm256_extractf128_si256::<0>(c),
|
||||
);
|
||||
$index += $stride;
|
||||
// second vector
|
||||
_mm_storeu_si128(
|
||||
($out)
|
||||
.get_mut($index..$index + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
_mm256_extractf128_si256::<1>(c),
|
||||
);
|
||||
$index += $stride;
|
||||
};
|
||||
}
|
||||
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::similar_names,
|
||||
clippy::op_ref,
|
||||
unused_assignments,
|
||||
clippy::zero_prefixed_literal
|
||||
)]
|
||||
pub unsafe fn idct_avx2(
|
||||
in_vector: &mut [i32; 64], out_vector: &mut [i16], stride: usize,
|
||||
) {
|
||||
let mut pos = 0;
|
||||
|
||||
// load into registers
|
||||
//
|
||||
// We sign extend i16's to i32's and calculate them with extended precision and
|
||||
// later reduce them to i16's when we are done carrying out IDCT
|
||||
|
||||
let rw0 = _mm256_loadu_si256(in_vector[00..].as_ptr().cast());
|
||||
let rw1 = _mm256_loadu_si256(in_vector[08..].as_ptr().cast());
|
||||
let rw2 = _mm256_loadu_si256(in_vector[16..].as_ptr().cast());
|
||||
let rw3 = _mm256_loadu_si256(in_vector[24..].as_ptr().cast());
|
||||
let rw4 = _mm256_loadu_si256(in_vector[32..].as_ptr().cast());
|
||||
let rw5 = _mm256_loadu_si256(in_vector[40..].as_ptr().cast());
|
||||
let rw6 = _mm256_loadu_si256(in_vector[48..].as_ptr().cast());
|
||||
let rw7 = _mm256_loadu_si256(in_vector[56..].as_ptr().cast());
|
||||
|
||||
// Forward DCT and quantization may cause all the AC terms to be zero, for such
|
||||
// cases we can try to accelerate it
|
||||
|
||||
// Basically the poop is that whenever the array has 63 zeroes, its idct is
|
||||
// (arr[0]>>3)or (arr[0]/8) propagated to all the elements.
|
||||
// We first test to see if the array contains zero elements and if it does, we go the
|
||||
// short way.
|
||||
//
|
||||
// This reduces IDCT overhead from about 39% to 18 %, almost half
|
||||
|
||||
// Do another load for the first row, we don't want to check DC value, because
|
||||
// we only care about AC terms
|
||||
let rw8 = _mm256_loadu_si256(in_vector[1..].as_ptr().cast());
|
||||
|
||||
let mut bitmap = _mm256_or_si256(rw1, rw2);
|
||||
bitmap = _mm256_or_si256(bitmap, rw3);
|
||||
bitmap = _mm256_or_si256(bitmap, rw4);
|
||||
bitmap = _mm256_or_si256(bitmap, rw5);
|
||||
bitmap = _mm256_or_si256(bitmap, rw6);
|
||||
bitmap = _mm256_or_si256(bitmap, rw7);
|
||||
bitmap = _mm256_or_si256(bitmap, rw8);
|
||||
|
||||
if _mm256_testz_si256(bitmap, bitmap) == 1 {
|
||||
// AC terms all zero, idct of the block is ( coeff[0] * qt[0] )/8 + 128 (bias)
|
||||
// (and clamped to 255)
|
||||
// Round by adding 0.5 * (1 << 3) and offset by adding (128 << 3) before scaling
|
||||
let coeff = ((in_vector[0] + 4 + 1024) >> 3).clamp(0, 255) as i16;
|
||||
let idct_value = _mm_set1_epi16(coeff);
|
||||
|
||||
macro_rules! store {
|
||||
($pos:tt,$value:tt) => {
|
||||
// store
|
||||
_mm_storeu_si128(
|
||||
out_vector
|
||||
.get_mut($pos..$pos + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
$value,
|
||||
);
|
||||
$pos += stride;
|
||||
};
|
||||
}
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let mut row0 = YmmRegister { mm256: rw0 };
|
||||
let mut row1 = YmmRegister { mm256: rw1 };
|
||||
let mut row2 = YmmRegister { mm256: rw2 };
|
||||
let mut row3 = YmmRegister { mm256: rw3 };
|
||||
|
||||
let mut row4 = YmmRegister { mm256: rw4 };
|
||||
let mut row5 = YmmRegister { mm256: rw5 };
|
||||
let mut row6 = YmmRegister { mm256: rw6 };
|
||||
let mut row7 = YmmRegister { mm256: rw7 };
|
||||
|
||||
macro_rules! dct_pass {
|
||||
($SCALE_BITS:tt,$scale:tt) => {
|
||||
// There are a lot of ways to do this
|
||||
// but to keep it simple(and beautiful), ill make a direct translation of the
|
||||
// scalar code to also make this code fully transparent(this version and the non
|
||||
// avx one should produce identical code.)
|
||||
|
||||
// even part
|
||||
let p1 = (row2 + row6) * 2217;
|
||||
|
||||
let mut t2 = p1 + row6 * -7567;
|
||||
let mut t3 = p1 + row2 * 3135;
|
||||
|
||||
let mut t0 = YmmRegister {
|
||||
mm256: _mm256_slli_epi32((row0 + row4).mm256, 12),
|
||||
};
|
||||
let mut t1 = YmmRegister {
|
||||
mm256: _mm256_slli_epi32((row0 - row4).mm256, 12),
|
||||
};
|
||||
|
||||
let x0 = t0 + t3 + $SCALE_BITS;
|
||||
let x3 = t0 - t3 + $SCALE_BITS;
|
||||
let x1 = t1 + t2 + $SCALE_BITS;
|
||||
let x2 = t1 - t2 + $SCALE_BITS;
|
||||
|
||||
let p3 = row7 + row3;
|
||||
let p4 = row5 + row1;
|
||||
let p1 = row7 + row1;
|
||||
let p2 = row5 + row3;
|
||||
let p5 = (p3 + p4) * 4816;
|
||||
|
||||
t0 = row7 * 1223;
|
||||
t1 = row5 * 8410;
|
||||
t2 = row3 * 12586;
|
||||
t3 = row1 * 6149;
|
||||
|
||||
let p1 = p5 + p1 * -3685;
|
||||
let p2 = p5 + (p2 * -10497);
|
||||
let p3 = p3 * -8034;
|
||||
let p4 = p4 * -1597;
|
||||
|
||||
t3 += p1 + p4;
|
||||
t2 += p2 + p3;
|
||||
t1 += p2 + p4;
|
||||
t0 += p1 + p3;
|
||||
|
||||
row0.mm256 = _mm256_srai_epi32((x0 + t3).mm256, $scale);
|
||||
row1.mm256 = _mm256_srai_epi32((x1 + t2).mm256, $scale);
|
||||
row2.mm256 = _mm256_srai_epi32((x2 + t1).mm256, $scale);
|
||||
row3.mm256 = _mm256_srai_epi32((x3 + t0).mm256, $scale);
|
||||
|
||||
row4.mm256 = _mm256_srai_epi32((x3 - t0).mm256, $scale);
|
||||
row5.mm256 = _mm256_srai_epi32((x2 - t1).mm256, $scale);
|
||||
row6.mm256 = _mm256_srai_epi32((x1 - t2).mm256, $scale);
|
||||
row7.mm256 = _mm256_srai_epi32((x0 - t3).mm256, $scale);
|
||||
};
|
||||
}
|
||||
|
||||
// Process rows
|
||||
dct_pass!(512, 10);
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7,
|
||||
);
|
||||
|
||||
// process columns
|
||||
dct_pass!(SCALE_BITS, 17);
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7,
|
||||
);
|
||||
// Pack and write the values back to the array
|
||||
permute_store!((row0.mm256), (row1.mm256), pos, out_vector, stride);
|
||||
permute_store!((row2.mm256), (row3.mm256), pos, out_vector, stride);
|
||||
permute_store!((row4.mm256), (row5.mm256), pos, out_vector, stride);
|
||||
permute_store!((row6.mm256), (row7.mm256), pos, out_vector, stride);
|
||||
}
|
||||
|
||||
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::similar_names,
|
||||
clippy::op_ref,
|
||||
unused_assignments,
|
||||
clippy::zero_prefixed_literal
|
||||
)]
|
||||
pub unsafe fn idct_avx2_4x4(
|
||||
in_vector: &mut [i32; 64], out_vector: &mut [i16], stride: usize,
|
||||
) {
|
||||
let rw0 = _mm256_loadu_si256(in_vector[00..].as_ptr().cast());
|
||||
let rw1 = _mm256_loadu_si256(in_vector[08..].as_ptr().cast());
|
||||
let rw2 = _mm256_loadu_si256(in_vector[16..].as_ptr().cast());
|
||||
let rw3 = _mm256_loadu_si256(in_vector[24..].as_ptr().cast());
|
||||
|
||||
let mut row0 = YmmRegister { mm256: rw0 };
|
||||
let mut row1 = YmmRegister { mm256: rw1 };
|
||||
let mut row2 = YmmRegister { mm256: rw2 };
|
||||
let mut row3 = YmmRegister { mm256: rw3 };
|
||||
|
||||
let mut row4 = YmmRegister { mm256: rw0 };
|
||||
let mut row5 = YmmRegister { mm256: rw0 };
|
||||
let mut row6 = YmmRegister { mm256: rw0 };
|
||||
let mut row7 = YmmRegister { mm256: rw0 };
|
||||
|
||||
{
|
||||
row0.mm256 = _mm256_slli_epi32(row0.mm256, 12);
|
||||
row0 += 512;
|
||||
|
||||
let i2 = row2;
|
||||
|
||||
let p1 = i2 * 2217;
|
||||
let p3 = i2 * 5352;
|
||||
|
||||
let x0 = row0 + p3;
|
||||
let x1 = row0 + p1;
|
||||
let x2 = row0 - p1;
|
||||
let x3 = row0 - p3;
|
||||
|
||||
// odd part
|
||||
let i4 = row3;
|
||||
let i3 = row1;
|
||||
|
||||
let p5 = (i4 + i3) * 4816;
|
||||
|
||||
let p1 = p5 + i3 * -3685;
|
||||
let p2 = p5 + i4 * -10497;
|
||||
|
||||
let t3 = p5 + i3 * 867;
|
||||
let t2 = p5 + i4 * -5945;
|
||||
|
||||
let t1 = p2 + i3 * -1597;
|
||||
let t0 = p1 + i4 * -8034;
|
||||
|
||||
row0.mm256 = _mm256_srai_epi32((x0 + t3).mm256, 10);
|
||||
row1.mm256 = _mm256_srai_epi32((x1 + t2).mm256, 10);
|
||||
row2.mm256 = _mm256_srai_epi32((x2 + t1).mm256, 10);
|
||||
row3.mm256 = _mm256_srai_epi32((x3 + t0).mm256, 10);
|
||||
|
||||
row4.mm256 = _mm256_srai_epi32((x3 - t0).mm256, 10);
|
||||
row5.mm256 = _mm256_srai_epi32((x2 - t1).mm256, 10);
|
||||
row6.mm256 = _mm256_srai_epi32((x1 - t2).mm256, 10);
|
||||
row7.mm256 = _mm256_srai_epi32((x0 - t3).mm256, 10);
|
||||
}
|
||||
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7,
|
||||
);
|
||||
|
||||
{
|
||||
let i2 = row2;
|
||||
let i0 = row0;
|
||||
|
||||
row0.mm256 = _mm256_slli_epi32(i0.mm256, 12);
|
||||
let t0 = row0 + SCALE_BITS;
|
||||
|
||||
let t2 = i2 * 2217;
|
||||
let t3 = i2 * 5352;
|
||||
|
||||
// constants scaled things up by 1<<12, plus we had 1<<2 from first
|
||||
// loop, plus horizontal and vertical each scale by sqrt(8) so together
|
||||
// we've got an extra 1<<3, so 1<<17 total we need to remove.
|
||||
// so we want to round that, which means adding 0.5 * 1<<17,
|
||||
// aka 65536. Also, we'll end up with -128 to 127 that we want
|
||||
// to encode as 0..255 by adding 128, so we'll add that before the shift
|
||||
// Rounding constant is already added into `t0`
|
||||
let x0 = t0 + t3;
|
||||
let x3 = t0 - t3;
|
||||
let x1 = t0 + t2;
|
||||
let x2 = t0 - t2;
|
||||
|
||||
// odd part
|
||||
let i3 = row3;
|
||||
let i1 = row1;
|
||||
|
||||
let p5 = (i3 + i1) * 4816;
|
||||
|
||||
let p1 = p5 + i1 * -3685;
|
||||
let p2 = p5 + i3 * -10497;
|
||||
|
||||
let t3 = p5 + i1 * 867;
|
||||
let t2 = p5 + i3 * -5945;
|
||||
|
||||
let t1 = p2 + i1 * -1597;
|
||||
let t0 = p1 + i3 * -8034;
|
||||
|
||||
row0.mm256 = _mm256_srai_epi32((x0 + t3).mm256, 17);
|
||||
row1.mm256 = _mm256_srai_epi32((x1 + t2).mm256, 17);
|
||||
row2.mm256 = _mm256_srai_epi32((x2 + t1).mm256, 17);
|
||||
row3.mm256 = _mm256_srai_epi32((x3 + t0).mm256, 17);
|
||||
row4.mm256 = _mm256_srai_epi32((x3 - t0).mm256, 17);
|
||||
row5.mm256 = _mm256_srai_epi32((x2 - t1).mm256, 17);
|
||||
row6.mm256 = _mm256_srai_epi32((x1 - t2).mm256, 17);
|
||||
row7.mm256 = _mm256_srai_epi32((x0 - t3).mm256, 17);
|
||||
}
|
||||
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7,
|
||||
);
|
||||
|
||||
let mut pos = 0;
|
||||
|
||||
// Pack and write the values back to the array
|
||||
permute_store!((row0.mm256), (row1.mm256), pos, out_vector, stride);
|
||||
permute_store!((row2.mm256), (row3.mm256), pos, out_vector, stride);
|
||||
permute_store!((row4.mm256), (row5.mm256), pos, out_vector, stride);
|
||||
permute_store!((row6.mm256), (row7.mm256), pos, out_vector, stride);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn clamp_avx(reg: __m256i) -> __m256i {
|
||||
let min_s = _mm256_set1_epi16(0);
|
||||
let max_s = _mm256_set1_epi16(255);
|
||||
|
||||
let max_v = _mm256_max_epi16(reg, min_s); //max(a,0)
|
||||
let min_v = _mm256_min_epi16(max_v, max_s); //min(max(a,0),255)
|
||||
return min_v;
|
||||
}
|
||||
|
||||
/// A copy of `_MM_SHUFFLE()` that doesn't require
|
||||
/// a nightly compiler
|
||||
#[inline]
|
||||
const fn shuffle(z: i32, y: i32, x: i32, w: i32) -> i32 {
|
||||
((z << 6) | (y << 4) | (x << 2) | w)
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![cfg(target_arch = "aarch64")]
|
||||
//! AVX optimised IDCT.
|
||||
//!
|
||||
//! Okay not thaat optimised.
|
||||
//!
|
||||
//!
|
||||
//! # The implementation
|
||||
//! The implementation is neatly broken down into two operations.
|
||||
//!
|
||||
//! 1. Test for zeroes
|
||||
//! > There is a shortcut method for idct where when all AC values are zero, we can get the answer really quickly.
|
||||
//! by scaling the 1/8th of the DCT coefficient of the block to the whole block and level shifting.
|
||||
//!
|
||||
//! 2. If above fails, we proceed to carry out IDCT as a two pass one dimensional algorithm.
|
||||
//! IT does two whole scans where it carries out IDCT on all items
|
||||
//! After each successive scan, data is transposed in register(thank you x86 SIMD powers). and the second
|
||||
//! pass is carried out.
|
||||
//!
|
||||
//! The code is not super optimized, it produces bit identical results with scalar code hence it's
|
||||
//! `mm256_add_epi16`
|
||||
//! and it also has the advantage of making this implementation easy to maintain.
|
||||
|
||||
#![cfg(feature = "neon")]
|
||||
|
||||
use core::arch::aarch64::*;
|
||||
|
||||
use crate::unsafe_utils::{transpose, YmmRegister};
|
||||
|
||||
const SCALE_BITS: i32 = 512 + 65536 + (128 << 17);
|
||||
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "neon")]
|
||||
unsafe fn pack_16(a: int32x4x2_t) -> int16x8_t {
|
||||
vcombine_s16(vqmovn_s32(a.0), vqmovn_s32(a.1))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "neon")]
|
||||
unsafe fn condense_bottom_16(a: int32x4x2_t, b: int32x4x2_t) -> int16x8x2_t {
|
||||
int16x8x2_t(pack_16(a), pack_16(b))
|
||||
}
|
||||
|
||||
#[target_feature(enable = "neon")]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::similar_names,
|
||||
clippy::op_ref,
|
||||
unused_assignments,
|
||||
clippy::zero_prefixed_literal
|
||||
)]
|
||||
pub unsafe fn idct_neon(
|
||||
in_vector: &mut [i32; 64], out_vector: &mut [i16], stride: usize
|
||||
) {
|
||||
let mut pos = 0;
|
||||
|
||||
// load into registers
|
||||
//
|
||||
// We sign extend i16's to i32's and calculate them with extended precision and
|
||||
// later reduce them to i16's when we are done carrying out IDCT
|
||||
|
||||
let mut row0 = YmmRegister::load(in_vector[00..].as_ptr().cast());
|
||||
let mut row1 = YmmRegister::load(in_vector[08..].as_ptr().cast());
|
||||
let mut row2 = YmmRegister::load(in_vector[16..].as_ptr().cast());
|
||||
let mut row3 = YmmRegister::load(in_vector[24..].as_ptr().cast());
|
||||
let mut row4 = YmmRegister::load(in_vector[32..].as_ptr().cast());
|
||||
let mut row5 = YmmRegister::load(in_vector[40..].as_ptr().cast());
|
||||
let mut row6 = YmmRegister::load(in_vector[48..].as_ptr().cast());
|
||||
let mut row7 = YmmRegister::load(in_vector[56..].as_ptr().cast());
|
||||
|
||||
// Forward DCT and quantization may cause all the AC terms to be zero, for such
|
||||
// cases we can try to accelerate it
|
||||
|
||||
// Basically the poop is that whenever the array has 63 zeroes, its idct is
|
||||
// (arr[0]>>3)or (arr[0]/8) propagated to all the elements.
|
||||
// We first test to see if the array contains zero elements and if it does, we go the
|
||||
// short way.
|
||||
//
|
||||
// This reduces IDCT overhead from about 39% to 18 %, almost half
|
||||
|
||||
// Do another load for the first row, we don't want to check DC value, because
|
||||
// we only care about AC terms
|
||||
// TODO this should be a shift/shuffle, not a likely unaligned load
|
||||
let row8 = YmmRegister::load(in_vector[1..].as_ptr().cast());
|
||||
|
||||
let or_tree = (((row1 | row8) | (row2 | row3)) | ((row4 | row5) | (row6 | row7)));
|
||||
|
||||
if or_tree.all_zero() {
|
||||
// AC terms all zero, idct of the block is ( coeff[0] * qt[0] )/8 + 128 (bias)
|
||||
// (and clamped to 255)
|
||||
// Round by adding 0.5 * (1 << 3) and offset by adding (128 << 3) before scaling
|
||||
let coeff = ((in_vector[0] + 4 + 1024) >> 3).clamp(0, 255) as i16;
|
||||
let idct_value = vdupq_n_s16(coeff);
|
||||
|
||||
macro_rules! store {
|
||||
($pos:tt,$value:tt) => {
|
||||
// store
|
||||
vst1q_s16(
|
||||
out_vector
|
||||
.get_mut($pos..$pos + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
$value
|
||||
);
|
||||
$pos += stride;
|
||||
};
|
||||
}
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
store!(pos, idct_value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
macro_rules! dct_pass {
|
||||
($SCALE_BITS:tt,$scale:tt) => {
|
||||
// There are a lot of ways to do this
|
||||
// but to keep it simple(and beautiful), ill make a direct translation of the
|
||||
// scalar code to also make this code fully transparent(this version and the non
|
||||
// avx one should produce identical code.)
|
||||
|
||||
// Compiler does a pretty good job of optimizing add + mul pairs
|
||||
// into multiply-acumulate pairs
|
||||
|
||||
// even part
|
||||
let p1 = (row2 + row6) * 2217;
|
||||
|
||||
let mut t2 = p1 + row6 * -7567;
|
||||
let mut t3 = p1 + row2 * 3135;
|
||||
|
||||
let mut t0 = (row0 + row4).const_shl::<12>();
|
||||
let mut t1 = (row0 - row4).const_shl::<12>();
|
||||
|
||||
let x0 = t0 + t3 + $SCALE_BITS;
|
||||
let x3 = t0 - t3 + $SCALE_BITS;
|
||||
let x1 = t1 + t2 + $SCALE_BITS;
|
||||
let x2 = t1 - t2 + $SCALE_BITS;
|
||||
|
||||
let p3 = row7 + row3;
|
||||
let p4 = row5 + row1;
|
||||
let p1 = row7 + row1;
|
||||
let p2 = row5 + row3;
|
||||
let p5 = (p3 + p4) * 4816;
|
||||
|
||||
t0 = row7 * 1223;
|
||||
t1 = row5 * 8410;
|
||||
t2 = row3 * 12586;
|
||||
t3 = row1 * 6149;
|
||||
|
||||
let p1 = p5 + p1 * -3685;
|
||||
let p2 = p5 + (p2 * -10497);
|
||||
let p3 = p3 * -8034;
|
||||
let p4 = p4 * -1597;
|
||||
|
||||
t3 += p1 + p4;
|
||||
t2 += p2 + p3;
|
||||
t1 += p2 + p4;
|
||||
t0 += p1 + p3;
|
||||
|
||||
row0 = (x0 + t3).const_shra::<$scale>();
|
||||
row1 = (x1 + t2).const_shra::<$scale>();
|
||||
row2 = (x2 + t1).const_shra::<$scale>();
|
||||
row3 = (x3 + t0).const_shra::<$scale>();
|
||||
|
||||
row4 = (x3 - t0).const_shra::<$scale>();
|
||||
row5 = (x2 - t1).const_shra::<$scale>();
|
||||
row6 = (x1 - t2).const_shra::<$scale>();
|
||||
row7 = (x0 - t3).const_shra::<$scale>();
|
||||
};
|
||||
}
|
||||
|
||||
// Process rows
|
||||
dct_pass!(512, 10);
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7
|
||||
);
|
||||
|
||||
// process columns
|
||||
dct_pass!(SCALE_BITS, 17);
|
||||
transpose(
|
||||
&mut row0, &mut row1, &mut row2, &mut row3, &mut row4, &mut row5, &mut row6, &mut row7
|
||||
);
|
||||
|
||||
// Pack i32 to i16's,
|
||||
// clamp them to be between 0-255
|
||||
// Undo shuffling
|
||||
// Store back to array
|
||||
|
||||
// This could potentially be reorganized to take advantage of the multi-register stores
|
||||
macro_rules! permute_store {
|
||||
($x:tt,$y:tt,$index:tt,$out:tt) => {
|
||||
let a = condense_bottom_16($x, $y);
|
||||
|
||||
// Clamp the values after packing, we can clamp more values at once
|
||||
let b = clamp256_neon(a);
|
||||
|
||||
// store first vector
|
||||
vst1q_s16(
|
||||
($out)
|
||||
.get_mut($index..$index + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
b.0
|
||||
);
|
||||
$index += stride;
|
||||
// second vector
|
||||
vst1q_s16(
|
||||
($out)
|
||||
.get_mut($index..$index + 8)
|
||||
.unwrap()
|
||||
.as_mut_ptr()
|
||||
.cast(),
|
||||
b.1
|
||||
);
|
||||
$index += stride;
|
||||
};
|
||||
}
|
||||
// Pack and write the values back to the array
|
||||
permute_store!((row0.mm256), (row1.mm256), pos, out_vector);
|
||||
permute_store!((row2.mm256), (row3.mm256), pos, out_vector);
|
||||
permute_store!((row4.mm256), (row5.mm256), pos, out_vector);
|
||||
permute_store!((row6.mm256), (row7.mm256), pos, out_vector);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "neon")]
|
||||
unsafe fn clamp_neon(reg: int16x8_t) -> int16x8_t {
|
||||
let min_s = vdupq_n_s16(0);
|
||||
let max_s = vdupq_n_s16(255);
|
||||
|
||||
let max_v = vmaxq_s16(reg, min_s); //max(a,0)
|
||||
let min_v = vminq_s16(max_v, max_s); //min(max(a,0),255)
|
||||
min_v
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "neon")]
|
||||
unsafe fn clamp256_neon(reg: int16x8x2_t) -> int16x8x2_t {
|
||||
int16x8x2_t(clamp_neon(reg.0), clamp_neon(reg.1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_neon_clamp_256() {
|
||||
unsafe {
|
||||
let vals: [i16; 16] = [-1, -2, -3, 4, 256, 257, 258, 240, -1, 290, 2, 3, 4, 5, 6, 7];
|
||||
let loaded = vld1q_s16_x2(vals.as_ptr().cast());
|
||||
let shuffled = clamp256_neon(loaded);
|
||||
|
||||
let mut result: [i16; 16] = [0; 16];
|
||||
|
||||
vst1q_s16_x2(result.as_mut_ptr().cast(), shuffled);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
[0, 0, 0, 4, 255, 255, 255, 240, 0, 255, 2, 3, 4, 5, 6, 7]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Platform independent IDCT algorithm
|
||||
//!
|
||||
//! Not as fast as AVX one.
|
||||
|
||||
const SCALE_BITS: i32 = 512 + 65536 + (128 << 17);
|
||||
|
||||
#[inline(always)]
|
||||
fn wa(a: i32, b: i32) -> i32 {
|
||||
a.wrapping_add(b)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ws(a: i32, b: i32) -> i32 {
|
||||
a.wrapping_sub(b)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn wm(a: i32, b: i32) -> i32 {
|
||||
a.wrapping_mul(b)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn idct_int_1x1(in_vector: &mut [i32; 64], mut out_vector: &mut [i16], stride: usize) {
|
||||
let coeff = ((wa(wa(in_vector[0], 4), 1024) >> 3).clamp(0, 255)) as i16;
|
||||
|
||||
out_vector[..8].fill(coeff);
|
||||
for _ in 0..7 {
|
||||
out_vector = &mut out_vector[stride..];
|
||||
out_vector[..8].fill(coeff);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::op_ref,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
pub fn idct_int(in_vector: &mut [i32; 64], out_vector: &mut [i16], stride: usize) {
|
||||
let mut pos = 0;
|
||||
let mut i = 0;
|
||||
|
||||
if &in_vector[1..] == &[0_i32; 63] {
|
||||
return idct_int_1x1(in_vector, out_vector, stride);
|
||||
}
|
||||
|
||||
// vertical pass
|
||||
for ptr in 0..8 {
|
||||
let p2 = in_vector[ptr + 16];
|
||||
let p3 = in_vector[ptr + 48];
|
||||
|
||||
let p1 = wm(wa(p2, p3), 2217);
|
||||
|
||||
let t2 = wa(p1, wm(p3, -7567));
|
||||
let t3 = wa(p1, wm(p2, 3135));
|
||||
|
||||
let p2 = in_vector[ptr];
|
||||
let p3 = in_vector[32 + ptr];
|
||||
|
||||
let t0 = fsh(wa(p2, p3));
|
||||
let t1 = fsh(ws(p2, p3));
|
||||
|
||||
let x0 = wa(wa(t0, t3), 512);
|
||||
let x3 = wa(ws(t0, t3), 512);
|
||||
let x1 = wa(wa(t1, t2), 512);
|
||||
let x2 = wa(ws(t1, t2), 512);
|
||||
|
||||
let mut t0 = in_vector[ptr + 56];
|
||||
let mut t1 = in_vector[ptr + 40];
|
||||
let mut t2 = in_vector[ptr + 24];
|
||||
let mut t3 = in_vector[ptr + 8];
|
||||
|
||||
let p3 = wa(t0, t2);
|
||||
let p4 = wa(t1, t3);
|
||||
let p1 = wa(t0, t3);
|
||||
let p2 = wa(t1, t2);
|
||||
let p5 = wm(wa(p3, p4), 4816);
|
||||
|
||||
t0 = wm(t0, 1223);
|
||||
t1 = wm(t1, 8410);
|
||||
t2 = wm(t2, 12586);
|
||||
t3 = wm(t3, 6149);
|
||||
|
||||
let p1 = wa(p5, wm(p1, -3685));
|
||||
let p2 = wa(p5, wm(p2, -10497));
|
||||
let p3 = wm(p3, -8034);
|
||||
let p4 = wm(p4, -1597);
|
||||
|
||||
t3 = wa(t3, wa(p1, p4));
|
||||
t2 = wa(t2, wa(p2, p3));
|
||||
t1 = wa(t1, wa(p2, p4));
|
||||
t0 = wa(t0, wa(p1, p3));
|
||||
|
||||
in_vector[ptr] = ws(wa(x0, t3), 0) >> 10;
|
||||
in_vector[ptr + 8] = ws(wa(x1, t2), 0) >> 10;
|
||||
in_vector[ptr + 16] = ws(wa(x2, t1), 0) >> 10;
|
||||
in_vector[ptr + 24] = ws(wa(x3, t0), 0) >> 10;
|
||||
in_vector[ptr + 32] = ws(ws(x3, t0), 0) >> 10;
|
||||
in_vector[ptr + 40] = ws(ws(x2, t1), 0) >> 10;
|
||||
in_vector[ptr + 48] = ws(ws(x1, t2), 0) >> 10;
|
||||
in_vector[ptr + 56] = ws(ws(x0, t3), 0) >> 10;
|
||||
}
|
||||
|
||||
// horizontal pass
|
||||
while i < 64 {
|
||||
let p2 = in_vector[i + 2];
|
||||
let p3 = in_vector[i + 6];
|
||||
|
||||
let p1 = wm(wa(p2, p3), 2217);
|
||||
let t2 = wa(p1, wm(p3, -7567));
|
||||
let t3 = wa(p1, wm(p2, 3135));
|
||||
|
||||
let p2 = in_vector[i];
|
||||
let p3 = in_vector[i + 4];
|
||||
|
||||
let t0 = fsh(wa(p2, p3));
|
||||
let t1 = fsh(ws(p2, p3));
|
||||
|
||||
let x0 = wa(wa(t0, t3), SCALE_BITS);
|
||||
let x3 = wa(ws(t0, t3), SCALE_BITS);
|
||||
let x1 = wa(wa(t1, t2), SCALE_BITS);
|
||||
let x2 = wa(ws(t1, t2), SCALE_BITS);
|
||||
|
||||
let mut t0 = in_vector[i + 7];
|
||||
let mut t1 = in_vector[i + 5];
|
||||
let mut t2 = in_vector[i + 3];
|
||||
let mut t3 = in_vector[i + 1];
|
||||
|
||||
let p3 = wa(t0, t2);
|
||||
let p4 = wa(t1, t3);
|
||||
let p1 = wa(t0, t3);
|
||||
let p2 = wa(t1, t2);
|
||||
let p5 = wm(wa(p3, p4), f2f(1.175875602));
|
||||
|
||||
t0 = wm(t0, 1223);
|
||||
t1 = wm(t1, 8410);
|
||||
t2 = wm(t2, 12586);
|
||||
t3 = wm(t3, 6149);
|
||||
|
||||
let p1 = wa(p5, wm(p1, -3685));
|
||||
let p2 = wa(p5, wm(p2, -10497));
|
||||
let p3 = wm(p3, -8034);
|
||||
let p4 = wm(p4, -1597);
|
||||
|
||||
t3 = wa(t3, wa(p1, p4));
|
||||
t2 = wa(t2, wa(p2, p3));
|
||||
t1 = wa(t1, wa(p2, p4));
|
||||
t0 = wa(t0, wa(p1, p3));
|
||||
|
||||
let out: &mut [i16; 8] = out_vector
|
||||
.get_mut(pos..pos + 8)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
out[0] = clamp(wa(x0, t3) >> 17);
|
||||
out[1] = clamp(wa(x1, t2) >> 17);
|
||||
out[2] = clamp(wa(x2, t1) >> 17);
|
||||
out[3] = clamp(wa(x3, t0) >> 17);
|
||||
out[4] = clamp(ws(x3, t0) >> 17);
|
||||
out[5] = clamp(ws(x2, t1) >> 17);
|
||||
out[6] = clamp(ws(x1, t2) >> 17);
|
||||
out[7] = clamp(ws(x0, t3) >> 17);
|
||||
|
||||
i += 8;
|
||||
pos += stride;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
/// Multiply a number by 4096
|
||||
fn f2f(x: f32) -> i32 {
|
||||
(x * 4096.0 + 0.5) as i32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Multiply a number by 4096
|
||||
fn fsh(x: i32) -> i32 {
|
||||
x << 12
|
||||
}
|
||||
|
||||
/// Clamp values between 0 and 255
|
||||
#[inline]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn clamp(a: i32) -> i16 {
|
||||
a.clamp(0, 255) as i16
|
||||
}
|
||||
|
||||
/// IDCT assuming only the upper 4x4 is filled.
|
||||
pub fn idct4x4(in_vector: &mut [i32; 64], out_vector: &mut [i16], stride: usize) {
|
||||
let mut pos = 0;
|
||||
|
||||
// vertical pass
|
||||
for ptr in 0..4 {
|
||||
let i0 = wa(fsh(in_vector[ptr]), 512);
|
||||
let i2 = in_vector[ptr + 16];
|
||||
|
||||
let p1 = wm(i2, 2217);
|
||||
let p3 = wm(i2, 5352);
|
||||
|
||||
let x0 = wa(i0, p3);
|
||||
let x1 = wa(i0, p1);
|
||||
let x2 = ws(i0, p1);
|
||||
let x3 = ws(i0, p3);
|
||||
|
||||
// odd part
|
||||
let i4 = in_vector[ptr + 24];
|
||||
let i3 = in_vector[ptr + 8];
|
||||
|
||||
let p5 = wm(wa(i4, i3), 4816);
|
||||
|
||||
let p1 = wa(p5, wm(i3, -3685));
|
||||
let p2 = wa(p5, wm(i4, -10497));
|
||||
|
||||
let t3 = wa(p5, wm(i3, 867));
|
||||
let t2 = wa(p5, wm(i4, -5945));
|
||||
|
||||
let t1 = wa(p2, wm(i3, -1597));
|
||||
let t0 = wa(p1, wm(i4, -8034));
|
||||
|
||||
in_vector[ptr] = wa(x0, t3) >> 10;
|
||||
in_vector[ptr + 8] = wa(x1, t2) >> 10;
|
||||
in_vector[ptr + 16] = wa(x2, t1) >> 10;
|
||||
in_vector[ptr + 24] = wa(x3, t0) >> 10;
|
||||
in_vector[ptr + 32] = ws(x3, t0) >> 10;
|
||||
in_vector[ptr + 40] = ws(x2, t1) >> 10;
|
||||
in_vector[ptr + 48] = ws(x1, t2) >> 10;
|
||||
in_vector[ptr + 56] = ws(x0, t3) >> 10;
|
||||
}
|
||||
|
||||
// horizontal pass
|
||||
for i in (0..8).map(|i| 8 * i) {
|
||||
let i2 = in_vector[i + 2];
|
||||
let i0 = in_vector[i];
|
||||
|
||||
let t0 = wa(fsh(i0), SCALE_BITS);
|
||||
let t2 = wm(i2, 2217);
|
||||
let t3 = wm(i2, 5352);
|
||||
|
||||
let x0 = wa(t0, t3);
|
||||
let x3 = ws(t0, t3);
|
||||
let x1 = wa(t0, t2);
|
||||
let x2 = ws(t0, t2);
|
||||
|
||||
// odd part
|
||||
let i3 = in_vector[i + 3];
|
||||
let i1 = in_vector[i + 1];
|
||||
|
||||
let p5 = wm(wa(i3, i1), f2f(1.175875602));
|
||||
|
||||
let p1 = wa(p5, wm(i1, -3685));
|
||||
let p2 = wa(p5, wm(i3, -10497));
|
||||
|
||||
let t3 = wa(p5, wm(i1, 867));
|
||||
let t2 = wa(p5, wm(i3, -5945));
|
||||
|
||||
let t1 = wa(p2, wm(i1, -1597));
|
||||
let t0 = wa(p1, wm(i3, -8034));
|
||||
|
||||
let out: &mut [i16; 8] = out_vector
|
||||
.get_mut(pos..pos + 8)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
out.copy_from_slice(&[
|
||||
clamp(wa(x0, t3) >> 17),
|
||||
clamp(wa(x1, t2) >> 17),
|
||||
clamp(wa(x2, t1) >> 17),
|
||||
clamp(wa(x3, t0) >> 17),
|
||||
clamp(ws(x3, t0) >> 17),
|
||||
clamp(ws(x2, t1) >> 17),
|
||||
clamp(ws(x1, t2) >> 17),
|
||||
clamp(ws(x0, t3) >> 17),
|
||||
]);
|
||||
|
||||
pos += stride;
|
||||
}
|
||||
|
||||
in_vector[32..36].fill(0);
|
||||
in_vector[40..44].fill(0);
|
||||
in_vector[48..52].fill(0);
|
||||
in_vector[56..60].fill(0);
|
||||
}
|
||||
Vendored
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//!This crate provides a library for decoding valid
|
||||
//! ITU-T Rec. T.851 (09/2005) ITU-T T.81 (JPEG-1) or JPEG images.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! # Features
|
||||
//! - SSE and AVX accelerated functions to speed up certain decoding operations
|
||||
//! - FAST and accurate 32 bit IDCT algorithm
|
||||
//! - Fast color convert functions
|
||||
//! - RGBA and RGBX (4-Channel) color conversion functions
|
||||
//! - YCbCr to Luma(Grayscale) conversion.
|
||||
//!
|
||||
//! # Usage
|
||||
//! Add zune-jpeg to the dependencies in the project Cargo.toml
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! zune_jpeg = "0.5"
|
||||
//! ```
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Decode a JPEG file with default arguments.
|
||||
//!```no_run
|
||||
//! use std::fs::read;
|
||||
//! use std::io::BufReader;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//! let file_contents = BufReader::new(std::fs::File::open("a_jpeg.file").unwrap());
|
||||
//! let mut decoder = JpegDecoder::new(file_contents);
|
||||
//! let mut pixels = decoder.decode().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Migrating from version 0.4--
|
||||
//!
|
||||
//! ### Motivation
|
||||
//! zune v 0.5 reworks mainly the internal architecture of how we perform I/O
|
||||
//! ,before the decoder accepted byte slices that represent the whole data as contiguous
|
||||
//! but that was not ideal for all use cases, increasing memory e.g on massive files that had
|
||||
//! to be read to memory.
|
||||
//!
|
||||
//! With v 0.5 a new I/O system is introduced, which generally introduces mechanisms to process
|
||||
//! `std::io::Read + std::io::Seek` type of data feeds, (but which works in no-std), which means...
|
||||
//!
|
||||
//! ### What changes
|
||||
//!
|
||||
//! I/O code that looked like this
|
||||
//!
|
||||
//!```ignore
|
||||
//! use zune_core::colorspace::ColorSpace;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//! // Read file into memory
|
||||
//! let image = std::fs::read("image.jpg").unwrap();
|
||||
//! // Make a decoder from the slice
|
||||
//! let mut decoder = JpegDecoder::new(&image);
|
||||
//! // decode
|
||||
//! decoder.decode().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! Now can be rewritten in two ways.
|
||||
//!
|
||||
//! 1. File I/O (Using bufreader)
|
||||
//!
|
||||
//!```no_run
|
||||
//! use std::io::BufReader;
|
||||
//! use zune_core::colorspace::ColorSpace;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//!
|
||||
//! let image = BufReader::new(std::fs::File::open("image.jpg").unwrap());
|
||||
//! let mut decoder = JpegDecoder::new(image);
|
||||
//! // decode
|
||||
//! decoder.decode().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! 2. Reading to memory (but wrapping it in a Cursor like object)
|
||||
//!```no_run
|
||||
//! use zune_core::bytestream::ZCursor;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//!
|
||||
//! let image_data =std::fs::read("image.jpg").unwrap();
|
||||
//! // Alternatively, you can use std::io::Cursor,
|
||||
//! // but it is better speed wise to use ZCursor, and it also works in
|
||||
//! // no-std environments
|
||||
//! let mut cursor = ZCursor::new(image_data);
|
||||
//! // use the wrapped item
|
||||
//! let mut decoder = JpegDecoder::new(cursor);
|
||||
//! // decode
|
||||
//! decoder.decode().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! 3. Anything that implements [ZByteReaderTrait](zune_core::bytestream::traits::ZByteReaderTrait)
|
||||
//!
|
||||
//! ## Decode a JPEG file to RGBA format
|
||||
//!
|
||||
//! - Other (limited) supported formats are and BGR, BGRA
|
||||
//!
|
||||
//!```no_run
|
||||
//! use zune_core::bytestream::ZCursor;
|
||||
//! use zune_core::colorspace::ColorSpace;
|
||||
//! use zune_core::options::DecoderOptions;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//!
|
||||
//! let mut options = DecoderOptions::default().jpeg_set_out_colorspace(ColorSpace::RGBA);
|
||||
//!
|
||||
//! let mut decoder = JpegDecoder::new_with_options(ZCursor::new(&[]),options);
|
||||
//! let pixels = decoder.decode().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Decode an image and get its width and height.
|
||||
//!```no_run
|
||||
//! use zune_core::bytestream::ZCursor;
|
||||
//! use zune_jpeg::JpegDecoder;
|
||||
//!
|
||||
//! let mut decoder = JpegDecoder::new(ZCursor::new(&[]));
|
||||
//! decoder.decode_headers().unwrap();
|
||||
//! let image_info = decoder.info().unwrap();
|
||||
//! println!("{},{}",image_info.width,image_info.height)
|
||||
//! ```
|
||||
//! # Crate features.
|
||||
//! This crate tries to be as minimal as possible while being extensible
|
||||
//! enough to handle the complexities arising from parsing different types
|
||||
//! of jpeg images.
|
||||
//!
|
||||
//! Safety is a top concern that is why we provide both static ways to disable unsafe code,
|
||||
//! disabling x86 feature, and dynamic ,by using [`DecoderOptions::set_use_unsafe(false)`],
|
||||
//! both of these disable platform specific optimizations, which reduce the speed of decompression.
|
||||
//!
|
||||
//! Please do note that careful consideration has been taken to ensure that the unsafe paths
|
||||
//! are only unsafe because they depend on platform specific intrinsics, hence no need to disable them
|
||||
//!
|
||||
//! The crate tries to decode as many images as possible, as a best effort, even those violating the standard
|
||||
//! , this means a lot of images may get silent warnings and wrong output, but if you are sure you will be handling
|
||||
//! images that follow the spec, set `ZuneJpegOptions::set_strict` to true.
|
||||
//!
|
||||
//![`DecoderOptions::set_use_unsafe(false)`]: https://docs.rs/zune-core/latest/zune_core/options/struct.DecoderOptions.html#method.set_use_unsafe
|
||||
|
||||
#![warn(
|
||||
clippy::correctness,
|
||||
clippy::perf,
|
||||
clippy::pedantic,
|
||||
clippy::inline_always,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::panic
|
||||
)]
|
||||
#![allow(
|
||||
clippy::needless_return,
|
||||
clippy::similar_names,
|
||||
clippy::inline_always,
|
||||
clippy::similar_names,
|
||||
clippy::doc_markdown,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::missing_panics_doc,
|
||||
clippy::missing_errors_doc
|
||||
)]
|
||||
// no_std compatibility
|
||||
#![deny(clippy::std_instead_of_alloc, clippy::alloc_instead_of_core)]
|
||||
#![cfg_attr(not(any(feature = "x86", feature = "neon")), forbid(unsafe_code))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![cfg_attr(feature = "portable_simd", feature(portable_simd))]
|
||||
#![macro_use]
|
||||
extern crate alloc;
|
||||
extern crate core;
|
||||
|
||||
pub use zune_core;
|
||||
|
||||
pub use crate::components::SampleRatios;
|
||||
pub use crate::decoder::{ImageInfo, JpegDecoder};
|
||||
pub use crate::marker::Marker;
|
||||
mod bitstream;
|
||||
mod color_convert;
|
||||
mod components;
|
||||
mod decoder;
|
||||
pub mod errors;
|
||||
mod headers;
|
||||
mod huffman;
|
||||
#[cfg(not(fuzzing))]
|
||||
mod idct;
|
||||
#[cfg(fuzzing)]
|
||||
pub mod idct;
|
||||
mod marker;
|
||||
mod mcu;
|
||||
mod mcu_prog;
|
||||
mod misc;
|
||||
mod unsafe_utils;
|
||||
mod unsafe_utils_avx2;
|
||||
mod unsafe_utils_neon;
|
||||
mod upsampler;
|
||||
mod worker;
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![allow(clippy::upper_case_acronyms)]
|
||||
|
||||
/// JPEG Markers
|
||||
///
|
||||
/// **NOTE** This doesn't cover all markers, just the ones zune-jpeg supports.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Marker {
|
||||
/// Start Of Frame markers
|
||||
///
|
||||
/// - SOF(0): Baseline DCT (Huffman coding)
|
||||
/// - SOF(1): Extended sequential DCT (Huffman coding)
|
||||
/// - SOF(2): Progressive DCT (Huffman coding)
|
||||
/// - SOF(3): Lossless (sequential) (Huffman coding)
|
||||
/// - SOF(5): Differential sequential DCT (Huffman coding)
|
||||
/// - SOF(6): Differential progressive DCT (Huffman coding)
|
||||
/// - SOF(7): Differential lossless (sequential) (Huffman coding)
|
||||
/// - SOF(9): Extended sequential DCT (arithmetic coding)
|
||||
/// - SOF(10): Progressive DCT (arithmetic coding)
|
||||
/// - SOF(11): Lossless (sequential) (arithmetic coding)
|
||||
/// - SOF(13): Differential sequential DCT (arithmetic coding)
|
||||
/// - SOF(14): Differential progressive DCT (arithmetic coding)
|
||||
/// - SOF(15): Differential lossless (sequential) (arithmetic coding)
|
||||
SOF(u8),
|
||||
/// Define Huffman table(s)
|
||||
DHT,
|
||||
/// Define arithmetic coding conditioning(s)
|
||||
DAC,
|
||||
/// Restart with modulo 8 count `m`
|
||||
RST(u8),
|
||||
/// Start of image
|
||||
SOI,
|
||||
/// End of image
|
||||
EOI,
|
||||
/// Start of scan
|
||||
SOS,
|
||||
/// Define quantization table(s)
|
||||
DQT,
|
||||
/// Define number of lines
|
||||
DNL,
|
||||
/// Define restart interval
|
||||
DRI,
|
||||
/// Reserved for application segments
|
||||
APP(u8),
|
||||
/// Comment
|
||||
COM,
|
||||
/// Unknown markers
|
||||
UNKNOWN(u8)
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
pub fn from_u8(n: u8) -> Option<Marker> {
|
||||
use self::Marker::{APP, COM, DAC, DHT, DNL, DQT, DRI, EOI, RST, SOF, SOI, SOS, UNKNOWN};
|
||||
|
||||
match n {
|
||||
0xFE => Some(COM),
|
||||
0xC0 => Some(SOF(0)),
|
||||
0xC1 => Some(SOF(1)),
|
||||
0xC2 => Some(SOF(2)),
|
||||
0xC4 => Some(DHT),
|
||||
0xCC => Some(DAC),
|
||||
0xD0 => Some(RST(0)),
|
||||
0xD1 => Some(RST(1)),
|
||||
0xD2 => Some(RST(2)),
|
||||
0xD3 => Some(RST(3)),
|
||||
0xD4 => Some(RST(4)),
|
||||
0xD5 => Some(RST(5)),
|
||||
0xD6 => Some(RST(6)),
|
||||
0xD7 => Some(RST(7)),
|
||||
0xD8 => Some(SOI),
|
||||
0xD9 => Some(EOI),
|
||||
0xDA => Some(SOS),
|
||||
0xDB => Some(DQT),
|
||||
0xDC => Some(DNL),
|
||||
0xDD => Some(DRI),
|
||||
0xE0 => Some(APP(0)),
|
||||
0xE1 => Some(APP(1)),
|
||||
0xE2 => Some(APP(2)),
|
||||
0xED => Some(APP(13)),
|
||||
0xEE => Some(APP(14)),
|
||||
_ => Some(UNKNOWN(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+936
@@ -0,0 +1,936 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{format, vec};
|
||||
use core::cmp::min;
|
||||
|
||||
use zune_core::bytestream::ZByteReaderTrait;
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::colorspace::ColorSpace::Luma;
|
||||
use zune_core::log::{error, trace, warn};
|
||||
|
||||
use crate::bitstream::BitStream;
|
||||
use crate::components::SampleRatios;
|
||||
use crate::decoder::MAX_COMPONENTS;
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::marker::Marker;
|
||||
use crate::mcu_prog::get_marker;
|
||||
use crate::misc::{calculate_padded_width, setup_component_params};
|
||||
use crate::worker::{color_convert, upsample};
|
||||
use crate::JpegDecoder;
|
||||
|
||||
/// The size of a DC block for a MCU.
|
||||
|
||||
pub const DCT_BLOCK: usize = 64;
|
||||
|
||||
impl<T: ZByteReaderTrait> JpegDecoder<T> {
|
||||
/// Check for existence of DC and AC Huffman Tables
|
||||
pub(crate) fn check_tables(&self) -> Result<(), DecodeErrors> {
|
||||
// check that dc and AC tables exist outside the hot path
|
||||
for component in &self.components {
|
||||
let _ = &self
|
||||
.dc_huffman_tables
|
||||
.get(component.dc_huff_table)
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::HuffmanDecode(format!(
|
||||
"No Huffman DC table for component {:?} ",
|
||||
component.component_id
|
||||
))
|
||||
})?
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::HuffmanDecode(format!(
|
||||
"No DC table for component {:?}",
|
||||
component.component_id
|
||||
))
|
||||
})?;
|
||||
|
||||
let _ = &self
|
||||
.ac_huffman_tables
|
||||
.get(component.ac_huff_table)
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::HuffmanDecode(format!(
|
||||
"No Huffman AC table for component {:?} ",
|
||||
component.component_id
|
||||
))
|
||||
})?
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::HuffmanDecode(format!(
|
||||
"No AC table for component {:?}",
|
||||
component.component_id
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode MCUs and carry out post processing.
|
||||
///
|
||||
/// This is the main decoder loop for the library, the hot path.
|
||||
///
|
||||
/// Because of this, we pull in some very crazy optimization tricks hence readability is a pinch
|
||||
/// here.
|
||||
#[allow(
|
||||
clippy::similar_names,
|
||||
clippy::too_many_lines,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
#[inline(never)]
|
||||
pub(crate) fn decode_mcu_ycbcr_baseline(
|
||||
&mut self, pixels: &mut [u8]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
setup_component_params(self)?;
|
||||
|
||||
// check dc and AC tables
|
||||
self.check_tables()?;
|
||||
|
||||
let (mut mcu_width, mut mcu_height);
|
||||
|
||||
if self.is_interleaved {
|
||||
// set upsampling functions
|
||||
self.set_upsampling()?;
|
||||
|
||||
mcu_width = self.mcu_x;
|
||||
mcu_height = self.mcu_y;
|
||||
} else {
|
||||
// For non-interleaved images( (1*1) subsampling)
|
||||
// number of MCU's are the widths (+7 to account for paddings) divided bu 8.
|
||||
mcu_width = ((self.info.width + 7) / 8) as usize;
|
||||
mcu_height = ((self.info.height + 7) / 8) as usize;
|
||||
}
|
||||
if self.is_interleaved
|
||||
&& self.input_colorspace.num_components() > 1
|
||||
&& self.options.jpeg_get_out_colorspace().num_components() == 1
|
||||
&& (self.info.sample_ratio == SampleRatios::V
|
||||
|| self.info.sample_ratio == SampleRatios::HV)
|
||||
{
|
||||
// For a specific set of images, e.g interleaved,
|
||||
// when converting from YcbCr to grayscale, we need to
|
||||
// take into account mcu height since the MCU decoding needs to take
|
||||
// it into account for padding purposes and the post processor
|
||||
// parses two rows per mcu width.
|
||||
//
|
||||
// set coeff to be 2 to ensure that we increment two rows
|
||||
// for every mcu processed also
|
||||
mcu_height *= self.v_max;
|
||||
mcu_height /= self.h_max;
|
||||
self.coeff = 2;
|
||||
}
|
||||
|
||||
if self.input_colorspace == ColorSpace::Luma && self.is_interleaved {
|
||||
warn!("Grayscale image with down-sampled component, resetting component details");
|
||||
|
||||
self.reset_params();
|
||||
|
||||
mcu_width = ((self.info.width + 7) / 8) as usize;
|
||||
mcu_height = ((self.info.height + 7) / 8) as usize;
|
||||
}
|
||||
let width = usize::from(self.info.width);
|
||||
|
||||
let padded_width = calculate_padded_width(width, self.info.sample_ratio);
|
||||
|
||||
let mut stream = BitStream::new();
|
||||
let mut tmp = [0_i32; DCT_BLOCK];
|
||||
|
||||
let comp_len = self.components.len();
|
||||
|
||||
for (pos, comp) in self.components.iter_mut().enumerate() {
|
||||
// Allocate only needed components.
|
||||
//
|
||||
// For special colorspaces i.e YCCK and CMYK, just allocate all of the needed
|
||||
// components.
|
||||
if min(
|
||||
self.options.jpeg_get_out_colorspace().num_components() - 1,
|
||||
pos
|
||||
) == pos
|
||||
|| comp_len == 4
|
||||
// Special colorspace
|
||||
{
|
||||
// allocate enough space to hold a whole MCU width
|
||||
// this means we should take into account sampling ratios
|
||||
// `*8` is because each MCU spans 8 widths.
|
||||
let len = comp.width_stride * comp.vertical_sample * 8;
|
||||
|
||||
comp.needed = true;
|
||||
comp.raw_coeff = vec![0; len];
|
||||
} else {
|
||||
comp.needed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If all components are contained in the first scan of MCUs, then we can process into
|
||||
// (upsampled) pixels immediately after each MCU, for convenience we use each row of MCUS.
|
||||
// Otherwise, we must first wait until following SOS provide the remaining components.
|
||||
let all_components_in_first_scan = usize::from(self.num_scans) == self.components.len();
|
||||
let mut progressive_mcus: [Vec<i16>; 4] = core::array::from_fn(|_| vec![]);
|
||||
|
||||
if !all_components_in_first_scan {
|
||||
for (component, mcu) in self.components.iter().zip(&mut progressive_mcus) {
|
||||
let len = mcu_width
|
||||
* component.vertical_sample
|
||||
* component.horizontal_sample
|
||||
* mcu_height
|
||||
* 64;
|
||||
*mcu = vec![0; len];
|
||||
}
|
||||
}
|
||||
|
||||
let mut pixels_written = 0;
|
||||
|
||||
let is_hv = usize::from(self.is_interleaved);
|
||||
let upsampler_scratch_size = is_hv * self.components.iter().map(|x| x.width_stride).max().unwrap_or(0) * 8;
|
||||
let mut upsampler_scratch_space = vec![0; upsampler_scratch_size];
|
||||
|
||||
'sos: loop {
|
||||
trace!(
|
||||
"Baseline decoding of components: {:?}",
|
||||
&self.z_order[..usize::from(self.num_scans)]
|
||||
);
|
||||
|
||||
trace!("Decoding MCU width: {mcu_width}, height: {mcu_height}");
|
||||
|
||||
for i in 0..mcu_height {
|
||||
if stream.overread_by > 0 {
|
||||
pixels.get_mut(pixels_written..).map(|v| v.fill(128));
|
||||
if self.options.strict_mode() {
|
||||
return Err(DecodeErrors::FormatStatic("Premature end of buffer"));
|
||||
};
|
||||
|
||||
error!("Premature end of buffer");
|
||||
break;
|
||||
}
|
||||
|
||||
// decode a whole MCU width,
|
||||
// this takes into account interleaved components.
|
||||
let terminate = if all_components_in_first_scan {
|
||||
self.decode_mcu_width::<false>(
|
||||
mcu_width,
|
||||
i,
|
||||
&mut tmp,
|
||||
&mut stream,
|
||||
&mut progressive_mcus
|
||||
)?
|
||||
} else {
|
||||
/* NB: (cae). This code was added due to the issue at https://github.com/etemesi254/zune-image/issues/277
|
||||
*
|
||||
* There is a particular set of images that interleave the start of scan (SOS) with the MCU,
|
||||
* E.g if it's a three component image, we have SOS->MCU ->SOS->MCU ->SOS->MCU
|
||||
* which presents a problem on decoding, we need to buffer the whole image before continuing since
|
||||
* we won't have a row containing all the component data which will be needed e.g for color conversion.
|
||||
*
|
||||
* The mechanisms is that we decode the whole image upfront, which goes against the normal
|
||||
* routine of decoding MCU width , so this requires more memory upfront than initial routines
|
||||
* but it is a single image out of the many corpuses that exist, so its fine.
|
||||
* (image in test-images/jpeg/sos_news.jpeg)
|
||||
|
||||
* Code contributed by Aurelia Molzer (https://github.com/197g)
|
||||
|
||||
*
|
||||
*/
|
||||
|
||||
self.decode_mcu_width::<true>(
|
||||
mcu_width,
|
||||
i,
|
||||
&mut tmp,
|
||||
&mut stream,
|
||||
&mut progressive_mcus
|
||||
)?
|
||||
};
|
||||
|
||||
// process that width up until it's impossible. This is faster than allocation the
|
||||
// full components, which we skipped earlier.
|
||||
if all_components_in_first_scan {
|
||||
self.post_process(
|
||||
pixels,
|
||||
i,
|
||||
mcu_height,
|
||||
width,
|
||||
padded_width,
|
||||
&mut pixels_written,
|
||||
&mut upsampler_scratch_space
|
||||
)?;
|
||||
}
|
||||
|
||||
match terminate {
|
||||
McuContinuation::Ok => {}
|
||||
McuContinuation::AnotherSos if all_components_in_first_scan => {
|
||||
warn!("More than one SOS despite already having all components");
|
||||
return Ok(());
|
||||
}
|
||||
McuContinuation::AnotherSos => continue 'sos,
|
||||
McuContinuation::InterScanMarker(marker) => {
|
||||
// Handle inter-scan markers (DHT/DQT/etc) uniformly here.
|
||||
// This keeps all marker handling in the outer loop.
|
||||
if self.advance_to_next_sos(marker, &mut stream)? {
|
||||
continue 'sos;
|
||||
} else {
|
||||
// Hit EOI
|
||||
break;
|
||||
}
|
||||
}
|
||||
McuContinuation::Terminate => {
|
||||
warn!("Got terminate signal, will not process further");
|
||||
pixels.get_mut(pixels_written..).map(|v| v.fill(128));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Breaks if we get here, looping only if we have restarted, i.e. found another SOS and
|
||||
// continued at `'sos'.
|
||||
break;
|
||||
}
|
||||
|
||||
if !all_components_in_first_scan {
|
||||
self.finish_baseline_decoding(&progressive_mcus, mcu_width, pixels)?;
|
||||
}
|
||||
|
||||
// it may happen that some images don't have the whole buffer
|
||||
// so we can't panic in case of that
|
||||
// assert_eq!(pixels_written, pixels.len());
|
||||
|
||||
// For UHD usecases that tie two images separating them with EOI and
|
||||
// SOI markers, it may happen that we do not reach this image end of image
|
||||
// So this ensures we reach it
|
||||
// Ensure we read EOI
|
||||
if !stream.seen_eoi {
|
||||
let marker = get_marker(&mut self.stream, &mut stream);
|
||||
match marker {
|
||||
Ok(_m) => {
|
||||
trace!("Found marker {:?}", _m);
|
||||
}
|
||||
Err(_) => {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Finished decoding image");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process all MCUs when baseline decoding has been processing them component-after-component.
|
||||
/// For simplicity this assembles the dequantized blocks in the order that the post processing
|
||||
/// of an interleaved baseline decoding would use.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
pub(crate) fn finish_baseline_decoding(
|
||||
&mut self, block: &[Vec<i16>; MAX_COMPONENTS], _mcu_width: usize, pixels: &mut [u8]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
let mcu_height = self.mcu_y;
|
||||
|
||||
// Size of our output image(width*height)
|
||||
let is_hv = usize::from(self.is_interleaved);
|
||||
let upsampler_scratch_size = is_hv * self.components[0].width_stride;
|
||||
let width = usize::from(self.info.width);
|
||||
let padded_width = calculate_padded_width(width, self.info.sample_ratio);
|
||||
|
||||
let mut upsampler_scratch_space = vec![0; upsampler_scratch_size];
|
||||
|
||||
for (pos, comp) in self.components.iter_mut().enumerate() {
|
||||
// Mark only needed components for computing output colors.
|
||||
if min(
|
||||
self.options.jpeg_get_out_colorspace().num_components() - 1,
|
||||
pos
|
||||
) == pos
|
||||
|| self.input_colorspace == ColorSpace::YCCK
|
||||
|| self.input_colorspace == ColorSpace::CMYK
|
||||
{
|
||||
comp.needed = true;
|
||||
} else {
|
||||
comp.needed = false;
|
||||
}
|
||||
}
|
||||
|
||||
let mut pixels_written = 0;
|
||||
|
||||
// dequantize and idct have been performed, only color convert.
|
||||
for i in 0..mcu_height {
|
||||
// All the data is already in the right order, we just need to be able to pass it to
|
||||
// the post_process & upsample method. That expects all the data to be stored as one
|
||||
// row of MCUs in each component's `raw_coeff`.
|
||||
'component: for (position, component) in &mut self.components.iter_mut().enumerate() {
|
||||
if !component.needed {
|
||||
continue 'component;
|
||||
}
|
||||
|
||||
// step is the number of pixels this iteration wil be handling
|
||||
// Given by the number of mcu's height and the length of the component block
|
||||
// Since the component block contains the whole channel as raw pixels
|
||||
// we this evenly divides the pixels into MCU blocks
|
||||
//
|
||||
// For interleaved images, this gives us the exact pixels comprising a whole MCU
|
||||
// block
|
||||
let step = block[position].len() / mcu_height;
|
||||
|
||||
// where we will be reading our pixels from.
|
||||
let slice = &block[position][i * step..][..step];
|
||||
let temp_channel = &mut component.raw_coeff;
|
||||
temp_channel[..step].copy_from_slice(slice);
|
||||
}
|
||||
|
||||
// process that whole stripe of MCUs
|
||||
self.post_process(
|
||||
pixels,
|
||||
i,
|
||||
mcu_height,
|
||||
width,
|
||||
padded_width,
|
||||
&mut pixels_written,
|
||||
&mut upsampler_scratch_space
|
||||
)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn decode_mcu_width<const PROGRESSIVE: bool>(
|
||||
&mut self, mcu_width: usize, mcu_height: usize, tmp: &mut [i32; 64],
|
||||
stream: &mut BitStream, progressive: &mut [Vec<i16>; 4]
|
||||
) -> Result<McuContinuation, DecodeErrors> {
|
||||
let is_one_by_one = !self.scan_subsampled;
|
||||
|
||||
// The definition of MCU depends on the sampling factor of involved scans. When components
|
||||
// have different factors then each Minimal-Coding-Unit is the least common multiple such
|
||||
// that we have an integer number of blocks from each component. But the decoding of these
|
||||
// components differs from it otherwise, we need an inner loop with a dynamic amount of
|
||||
// coefficients per component, whereas otherwise we have exactly one block of coefficients
|
||||
// encoded for each component in the bitstream order.
|
||||
//
|
||||
// We statically specialize on this to improve code generation of the common case a little
|
||||
// bit. We could also special case common sub-sampling cases but be mindful of code bloat.
|
||||
if is_one_by_one {
|
||||
self.inner_decode_mcu_width::<PROGRESSIVE, false>(
|
||||
mcu_width,
|
||||
mcu_height,
|
||||
tmp,
|
||||
stream,
|
||||
progressive
|
||||
)
|
||||
} else {
|
||||
self.inner_decode_mcu_width::<PROGRESSIVE, true>(
|
||||
mcu_width,
|
||||
mcu_height,
|
||||
tmp,
|
||||
stream,
|
||||
progressive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Inline-never ensures we do get this function optimize on its own, into two different
|
||||
// versions, without the optimizer tripping up over the complexity that comes with the
|
||||
// constant folding. And constant folding is quite important for performance here as
|
||||
// when `not SAMPLED` then the inner loop has exactly one iteration per component in
|
||||
// the scan. The difference was ~1% or a bit more.
|
||||
fn inner_decode_mcu_width<const PROGRESSIVE: bool, const SAMPLED: bool>(
|
||||
&mut self, mcu_width: usize, mcu_height: usize, tmp: &mut [i32; 64],
|
||||
stream: &mut BitStream, progressive: &mut [Vec<i16>; 4]
|
||||
) -> Result<McuContinuation, DecodeErrors> {
|
||||
let z_order = self.z_order;
|
||||
let z_scans = &z_order[..usize::from(self.num_scans)];
|
||||
|
||||
// How much of the head of `tmp` was written by the last MCU decoding? We only check for
|
||||
// two different cases and not all possible outcomes as this is only used to optimize the
|
||||
// bytes written in `fill`. Since the clobber happens in UNZIGZAG order we'd be straddling
|
||||
// most cache lines anyways even if we did a partial write with the exact length of the
|
||||
// coefficient data which was written into `tmp`.
|
||||
let mut clobber_more_than_4x4 = true;
|
||||
|
||||
// For non-interleaved scans (PROGRESSIVE=true), each scan contains a single component
|
||||
// and we iterate over that component's actual data unit count, not the interleaved MCU
|
||||
// width multiplied by sampling factor.
|
||||
let scan_du_width = if PROGRESSIVE {
|
||||
let k = z_scans[0];
|
||||
let comp = &self.components[k];
|
||||
// Calculate actual data units for this component: ceil(width / (8 * subsampling_ratio))
|
||||
(self.info.width as usize * comp.horizontal_sample + self.h_max * 8 - 1)
|
||||
/ (self.h_max * 8)
|
||||
} else {
|
||||
mcu_width
|
||||
};
|
||||
|
||||
for j in 0..scan_du_width {
|
||||
// iterate over components
|
||||
for &k in z_scans {
|
||||
// we made this loop body massive due to several different paths that depend on
|
||||
// static conditions. Note we (potentially) call into other functions so the
|
||||
// compiler will not unroll anything here anyways. The gains from separating
|
||||
// differently optimized loop bodies are much greater than a single additional jump
|
||||
// here.
|
||||
let component = &mut self.components[k];
|
||||
|
||||
let dc_table = self.dc_huffman_tables[component.dc_huff_table % MAX_COMPONENTS]
|
||||
.as_ref()
|
||||
.ok_or(DecodeErrors::FormatStatic("DC table not found"))?;
|
||||
|
||||
let ac_table = self.ac_huffman_tables[component.ac_huff_table % MAX_COMPONENTS]
|
||||
.as_ref()
|
||||
.ok_or(DecodeErrors::FormatStatic("AC table not found"))?;
|
||||
|
||||
let qt_table = &component.quantization_table;
|
||||
let channel = if PROGRESSIVE {
|
||||
let offset =
|
||||
mcu_height * component.width_stride * 8 * component.vertical_sample;
|
||||
&mut progressive[k][offset..]
|
||||
} else {
|
||||
&mut component.raw_coeff
|
||||
};
|
||||
|
||||
let component_samples_needed = component.needed;
|
||||
|
||||
// If image is interleaved iterate over scan components,
|
||||
// otherwise if it-s non-interleaved, these routines iterate in
|
||||
// trivial scanline order(Y,Cb,Cr)
|
||||
//
|
||||
// Turn the bounds into a compile time constant for a common special case. This
|
||||
// allows the compiler to unroll the loop and then do a bunch of interleaving.
|
||||
//
|
||||
// For PROGRESSIVE (non-interleaved), we iterate data units directly so
|
||||
// h_samp/v_samp loops run exactly once.
|
||||
let v_step =
|
||||
if SAMPLED && !PROGRESSIVE { 0..component.vertical_sample } else { 0..1 };
|
||||
|
||||
for v_samp in v_step {
|
||||
let h_step =
|
||||
if SAMPLED && !PROGRESSIVE { 0..component.horizontal_sample } else { 0..1 };
|
||||
|
||||
for h_samp in h_step {
|
||||
let result = if component_samples_needed {
|
||||
// Fill the array with zeroes, decode_mcu_block expects
|
||||
// a zero based array. Clobber is in zig-zag order though.
|
||||
// Writing consecutive entries is basically free in terms
|
||||
// of memory throughput so we opt for a larger power of
|
||||
// two which lets the compiler turn this into a repeated
|
||||
// write of a zeroed vector register, which does not have
|
||||
// any branches, instead of a more difficult pattern where
|
||||
// we attempt to overwrite exactly one coefficient.
|
||||
let clobber_len = if !clobber_more_than_4x4 { 32 } else { 64 };
|
||||
|
||||
tmp[..clobber_len].fill(0);
|
||||
|
||||
stream.decode_mcu_block(
|
||||
&mut self.stream,
|
||||
dc_table,
|
||||
ac_table,
|
||||
qt_table,
|
||||
tmp,
|
||||
&mut component.dc_pred
|
||||
)
|
||||
} else {
|
||||
// We do not touch tmp so there is no need to reset it.
|
||||
stream.discard_mcu_block(&mut self.stream, dc_table, ac_table)
|
||||
};
|
||||
|
||||
// If an error occurs we can either propagate it
|
||||
// as an error or print it and call terminate.
|
||||
//
|
||||
// This allows even corrupt images to render something,
|
||||
// even if its bad, matching browsers.
|
||||
//
|
||||
// See example in https://github.com/etemesi254/zune-image/issues/293
|
||||
let len = if let Ok(len) = result {
|
||||
len
|
||||
} else {
|
||||
// result.is_err()
|
||||
return if self.options.strict_mode() {
|
||||
Err(result.err().unwrap())
|
||||
} else {
|
||||
error!("{}", result.err().unwrap());
|
||||
Ok(McuContinuation::Terminate)
|
||||
};
|
||||
};
|
||||
|
||||
if component_samples_needed {
|
||||
// tmp was only written partially, note that len is in ZigZag order.
|
||||
clobber_more_than_4x4 = len > 10;
|
||||
|
||||
let idct_position = if PROGRESSIVE {
|
||||
// For non-interleaved, j indexes data units directly
|
||||
j * 8
|
||||
} else {
|
||||
// derived from stb and rewritten for my tastes
|
||||
let c2 = v_samp * 8;
|
||||
let c3 = ((j * component.horizontal_sample) + h_samp) * 8;
|
||||
|
||||
component.width_stride * c2 + c3
|
||||
};
|
||||
|
||||
let idct_pos = channel.get_mut(idct_position..).unwrap();
|
||||
|
||||
if len <= 1 {
|
||||
(self.idct_1x1_func)(tmp, idct_pos, component.width_stride);
|
||||
} else if len <= 10 {
|
||||
(self.idct_4x4_func)(tmp, idct_pos, component.width_stride);
|
||||
} else {
|
||||
// call idct.
|
||||
(self.idct_func)(tmp, idct_pos, component.width_stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.todo = self.todo.wrapping_sub(1);
|
||||
|
||||
if self.todo == 0 {
|
||||
self.handle_rst_main(stream)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if stream.marker.is_some() && stream.bits_left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.check_stream_marker_after_mcu_width(stream)
|
||||
}
|
||||
|
||||
fn check_stream_marker_after_mcu_width(
|
||||
&mut self, stream: &mut BitStream
|
||||
) -> Result<McuContinuation, DecodeErrors> {
|
||||
// After all interleaved components, that's an MCU
|
||||
// handle stream markers
|
||||
//
|
||||
// In some corrupt images, it may occur that header markers occur in the stream.
|
||||
// The spec EXPLICITLY FORBIDS this, specifically, in
|
||||
// routine F.2.2.5 it says
|
||||
// `The only valid marker which may occur within the Huffman coded data is the RSTm marker.`
|
||||
//
|
||||
// But libjpeg-turbo allows it because of some weird reason. so I'll also
|
||||
// allow it because of some weird reason.
|
||||
if let Some(m) = stream.marker {
|
||||
if m == Marker::EOI {
|
||||
// acknowledge and ignore EOI marker.
|
||||
stream.marker.take();
|
||||
trace!("Found EOI marker");
|
||||
// Google Introduced the Ultra-HD image format which is basically
|
||||
// stitching two images into one container.
|
||||
// They basically separate two images via a EOI and SOI marker
|
||||
// so let's just ensure if we ever see EOI, we never read past that
|
||||
// ever.
|
||||
// https://github.com/google/libultrahdr
|
||||
stream.seen_eoi = true;
|
||||
} else if let Marker::RST(_) = m {
|
||||
//debug_assert_eq!(self.todo, 0);
|
||||
if self.todo == 0 {
|
||||
self.handle_rst(stream)?;
|
||||
}
|
||||
} else if let Marker::SOS = m {
|
||||
self.parse_marker_inner(m)?;
|
||||
stream.marker.take();
|
||||
stream.reset();
|
||||
trace!("Found SOS marker");
|
||||
return Ok(McuContinuation::AnotherSos);
|
||||
} else if matches!(m, Marker::DHT | Marker::DQT | Marker::DRI | Marker::COM)
|
||||
|| matches!(m, Marker::APP(_))
|
||||
{
|
||||
// For non-interleaved images, setup markers can appear between scans.
|
||||
// Signal the caller to handle this marker and find the next SOS.
|
||||
// This keeps all marker parsing in the caller's loop.
|
||||
stream.marker.take();
|
||||
trace!("Found inter-scan marker {:?}", m);
|
||||
return Ok(McuContinuation::InterScanMarker(m));
|
||||
} else {
|
||||
if self.options.strict_mode() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Marker {m:?} found where not expected"
|
||||
)));
|
||||
}
|
||||
error!(
|
||||
"Marker `{:?}` Found within Huffman Stream, possibly corrupt jpeg",
|
||||
m
|
||||
);
|
||||
|
||||
self.parse_marker_inner(m)?;
|
||||
stream.marker.take();
|
||||
stream.reset();
|
||||
return Ok(McuContinuation::Terminate);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(McuContinuation::Ok)
|
||||
}
|
||||
|
||||
/// Scan for the next SOS marker, parsing setup markers along the way.
|
||||
///
|
||||
/// This is the unified marker scanning function used after encountering an
|
||||
/// inter-scan marker. It handles DHT, DQT, DRI, COM, and APP markers that
|
||||
/// can appear between scans in non-interleaved images.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `first_marker` - The first marker that was already detected (not yet parsed)
|
||||
/// * `stream` - The bitstream state
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(true)` - Found SOS, ready to continue decoding
|
||||
/// * `Ok(false)` - Found EOI, decoding complete
|
||||
/// * `Err(_)` - Error (too many markers, unexpected marker in strict mode, etc.)
|
||||
fn advance_to_next_sos(
|
||||
&mut self,
|
||||
first_marker: Marker,
|
||||
stream: &mut BitStream
|
||||
) -> Result<bool, DecodeErrors> {
|
||||
// Limit iterations to prevent DoS from malicious files.
|
||||
const MAX_INTER_SCAN_MARKERS: usize = 64;
|
||||
|
||||
// Parse the first marker that triggered this call
|
||||
self.parse_marker_inner(first_marker)?;
|
||||
stream.reset();
|
||||
|
||||
for _ in 0..MAX_INTER_SCAN_MARKERS {
|
||||
let marker = get_marker(&mut self.stream, stream)?;
|
||||
|
||||
match marker {
|
||||
Marker::SOS => {
|
||||
self.parse_marker_inner(Marker::SOS)?;
|
||||
stream.reset();
|
||||
trace!("Found SOS marker, continuing decode");
|
||||
return Ok(true);
|
||||
}
|
||||
Marker::EOI => {
|
||||
stream.seen_eoi = true;
|
||||
trace!("Found EOI marker");
|
||||
return Ok(false);
|
||||
}
|
||||
Marker::DHT | Marker::DQT | Marker::DRI | Marker::COM => {
|
||||
trace!("Parsing inter-scan marker {:?}", marker);
|
||||
self.parse_marker_inner(marker)?;
|
||||
}
|
||||
Marker::APP(_) => {
|
||||
trace!("Parsing inter-scan APP marker {:?}", marker);
|
||||
self.parse_marker_inner(marker)?;
|
||||
}
|
||||
other => {
|
||||
if self.options.strict_mode() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Unexpected marker {:?} while scanning for SOS between scans",
|
||||
other
|
||||
)));
|
||||
}
|
||||
// Non-strict: skip unknown marker
|
||||
warn!("Skipping unexpected marker {:?} between scans", other);
|
||||
let length = self.stream.get_u16_be_err()?;
|
||||
if length >= 2 {
|
||||
self.stream.skip((length - 2) as usize)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(DecodeErrors::FormatStatic(
|
||||
"Too many markers between scans (exceeded limit of 64)"
|
||||
))
|
||||
}
|
||||
|
||||
// handle RST markers.
|
||||
// No-op if not using restarts
|
||||
// this routine is shared with mcu_prog
|
||||
#[cold]
|
||||
pub(crate) fn handle_rst(&mut self, stream: &mut BitStream) -> Result<(), DecodeErrors> {
|
||||
self.todo = self.restart_interval;
|
||||
|
||||
if let Some(marker) = stream.marker {
|
||||
// Found a marker
|
||||
// Read stream and see what marker is stored there
|
||||
match marker {
|
||||
Marker::RST(_) => {
|
||||
// reset stream
|
||||
stream.reset();
|
||||
// Initialize dc predictions to zero for all components
|
||||
self.components.iter_mut().for_each(|x| x.dc_pred = 0);
|
||||
// Start iterating again. from position.
|
||||
}
|
||||
Marker::EOI => {
|
||||
// silent pass
|
||||
}
|
||||
_ => {
|
||||
return Err(DecodeErrors::MCUError(format!(
|
||||
"Marker {marker:?} found in bitstream, possibly corrupt jpeg"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
|
||||
pub(crate) fn post_process(
|
||||
&mut self, pixels: &mut [u8], i: usize, mcu_height: usize, width: usize,
|
||||
padded_width: usize, pixels_written: &mut usize, upsampler_scratch_space: &mut [i16]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
let out_colorspace_components = self.options.jpeg_get_out_colorspace().num_components();
|
||||
|
||||
let mut px = *pixels_written;
|
||||
// indicates whether image is vertically up-sampled
|
||||
let is_vertically_sampled = self
|
||||
.components
|
||||
.iter()
|
||||
.any(|c| c.sample_ratio == SampleRatios::HV || c.sample_ratio == SampleRatios::V);
|
||||
|
||||
let mut comp_len = self.components.len();
|
||||
|
||||
// If we are moving from YCbCr -> Luma, we do not allocate storage for other components, so we
|
||||
// will panic when we are trying to read samples, so for that case,
|
||||
// hardcode it so that we don't panic when doing
|
||||
// *samp = &samples[j][pos * padded_width..(pos + 1) * padded_width]
|
||||
if out_colorspace_components < comp_len && self.options.jpeg_get_out_colorspace() == Luma {
|
||||
comp_len = out_colorspace_components;
|
||||
}
|
||||
let mut color_conv_function =
|
||||
|num_iters: usize, samples: [&[i16]; 4]| -> Result<(), DecodeErrors> {
|
||||
for (pos, output) in pixels[px..]
|
||||
.chunks_exact_mut(width * out_colorspace_components)
|
||||
.take(num_iters)
|
||||
.enumerate()
|
||||
{
|
||||
let mut raw_samples: [&[i16]; 4] = [&[], &[], &[], &[]];
|
||||
|
||||
// iterate over each line, since color-convert needs only
|
||||
// one line
|
||||
for (j, samp) in raw_samples.iter_mut().enumerate().take(comp_len) {
|
||||
let temp = &samples[j].get(pos * padded_width..(pos + 1) * padded_width);
|
||||
if temp.is_none() {
|
||||
return Err(DecodeErrors::FormatStatic("Missing samples"));
|
||||
}
|
||||
*samp = temp.unwrap();
|
||||
}
|
||||
color_convert(
|
||||
&raw_samples,
|
||||
self.color_convert_16,
|
||||
self.input_colorspace,
|
||||
self.options.jpeg_get_out_colorspace(),
|
||||
output,
|
||||
width,
|
||||
padded_width
|
||||
)?;
|
||||
px += width * out_colorspace_components;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let comps = &mut self.components[..];
|
||||
|
||||
if self.is_interleaved && self.options.jpeg_get_out_colorspace() != ColorSpace::Luma {
|
||||
for comp in comps.iter_mut() {
|
||||
upsample(
|
||||
comp,
|
||||
mcu_height,
|
||||
i,
|
||||
upsampler_scratch_space,
|
||||
is_vertically_sampled
|
||||
)?;
|
||||
}
|
||||
|
||||
if is_vertically_sampled {
|
||||
if i > 0 {
|
||||
// write the last line, it wasn't up-sampled as we didn't have row_down
|
||||
// yet
|
||||
let mut samples: [&[i16]; 4] = [&[], &[], &[], &[]];
|
||||
|
||||
for (samp, component) in samples.iter_mut().zip(comps.iter()) {
|
||||
*samp = &component.first_row_upsample_dest;
|
||||
}
|
||||
|
||||
// ensure length matches for all samples
|
||||
let _first_len = samples[0].len();
|
||||
|
||||
// This was a good check, but can be caused to panic, esp on invalid/corrupt images.
|
||||
// See one in issue https://github.com/etemesi254/zune-image/issues/262, so for now
|
||||
// we just ignore and generate invalid images at the end.
|
||||
|
||||
//
|
||||
//
|
||||
// for samp in samples.iter().take(comp_len) {
|
||||
// assert_eq!(first_len, samp.len());
|
||||
// }
|
||||
let num_iters = self.coeff * self.v_max;
|
||||
|
||||
color_conv_function(num_iters, samples)?;
|
||||
}
|
||||
|
||||
// After up-sampling the last row, save any row that can be used for
|
||||
// a later up-sampling,
|
||||
//
|
||||
// E.g the Y sample is not sampled but we haven't finished upsampling the last row of
|
||||
// the previous mcu, since we don't have the down row, so save it
|
||||
for component in comps.iter_mut() {
|
||||
if component.sample_ratio != SampleRatios::H {
|
||||
// We don't care about H sampling factors, since it's copied in the workers function
|
||||
|
||||
// copy last row to be used for the next color conversion
|
||||
let size = component.vertical_sample
|
||||
* component.width_stride
|
||||
* component.sample_ratio.sample();
|
||||
|
||||
let last_bytes =
|
||||
component.raw_coeff.rchunks_exact_mut(size).next().unwrap();
|
||||
|
||||
component
|
||||
.first_row_upsample_dest
|
||||
.copy_from_slice(last_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut samples: [&[i16]; 4] = [&[], &[], &[], &[]];
|
||||
|
||||
for (samp, component) in samples.iter_mut().zip(comps.iter()) {
|
||||
*samp = if component.sample_ratio == SampleRatios::None {
|
||||
&component.raw_coeff
|
||||
} else {
|
||||
&component.upsample_dest
|
||||
};
|
||||
}
|
||||
|
||||
// we either do 7 or 8 MCU's depending on the state, this only applies to
|
||||
// vertically sampled images
|
||||
//
|
||||
// for rows up until the last MCU, we do not upsample the last stride of the MCU
|
||||
// which means that the number of iterations should take that into account is one less the
|
||||
// up-sampled size
|
||||
//
|
||||
// For the last MCU, we upsample the last stride, meaning that if we hit the last MCU, we
|
||||
// should sample full raw coeffs
|
||||
let is_last_considered = is_vertically_sampled && (i != mcu_height.saturating_sub(1));
|
||||
|
||||
let num_iters = (8 - usize::from(is_last_considered)) * self.coeff * self.v_max;
|
||||
|
||||
color_conv_function(num_iters, samples)?;
|
||||
} else {
|
||||
let mut channels_ref: [&[i16]; MAX_COMPONENTS] = [&[]; MAX_COMPONENTS];
|
||||
|
||||
self.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(pos, x)| channels_ref[pos] = &x.raw_coeff);
|
||||
|
||||
if let SampleRatios::Generic(_, v) = self.info.sample_ratio {
|
||||
color_conv_function(8 * v * self.coeff, channels_ref)?;
|
||||
} else {
|
||||
color_conv_function(8 * self.coeff, channels_ref)?;
|
||||
}
|
||||
}
|
||||
|
||||
*pixels_written = px;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum McuContinuation {
|
||||
Ok,
|
||||
AnotherSos,
|
||||
/// Found an inter-scan marker (DHT/DQT/DRI/COM/APP) that needs handling.
|
||||
/// The caller should parse it and scan for the next SOS.
|
||||
InterScanMarker(Marker),
|
||||
Terminate
|
||||
}
|
||||
Vendored
+688
@@ -0,0 +1,688 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//!Routines for progressive decoding
|
||||
/*
|
||||
This file is needlessly complicated,
|
||||
|
||||
It is that way to ensure we don't burn memory anyhow
|
||||
|
||||
Memory is a scarce resource in some environments, I would like this to be viable
|
||||
in such environments
|
||||
|
||||
Half of the complexity comes from the jpeg spec, because progressive decoding,
|
||||
is one hell of a ride.
|
||||
|
||||
*/
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use alloc::{format, vec};
|
||||
use core::cmp::min;
|
||||
|
||||
use zune_core::bytestream::{ZByteReaderTrait, ZReader};
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::log::{debug, error, warn};
|
||||
|
||||
use crate::bitstream::BitStream;
|
||||
use crate::components::SampleRatios;
|
||||
use crate::decoder::{JpegDecoder, MAX_COMPONENTS};
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::headers::parse_sos;
|
||||
use crate::marker::Marker;
|
||||
use crate::mcu::DCT_BLOCK;
|
||||
use crate::misc::{calculate_padded_width, setup_component_params};
|
||||
|
||||
impl<T: ZByteReaderTrait> JpegDecoder<T> {
|
||||
/// Decode a progressive image
|
||||
///
|
||||
/// This routine decodes a progressive image, stopping if it finds any error.
|
||||
#[allow(
|
||||
clippy::needless_range_loop,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::redundant_else,
|
||||
clippy::too_many_lines
|
||||
)]
|
||||
#[inline(never)]
|
||||
pub(crate) fn decode_mcu_ycbcr_progressive(
|
||||
&mut self, pixels: &mut [u8]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
setup_component_params(self)?;
|
||||
|
||||
let mut mcu_height;
|
||||
|
||||
// memory location for decoded pixels for components
|
||||
let mut block: [Vec<i16>; MAX_COMPONENTS] = [vec![], vec![], vec![], vec![]];
|
||||
let mut mcu_width;
|
||||
|
||||
let mut seen_scans = 1;
|
||||
|
||||
if self.input_colorspace == ColorSpace::Luma && self.is_interleaved {
|
||||
warn!("Grayscale image with down-sampled component, resetting component details");
|
||||
self.reset_params();
|
||||
}
|
||||
|
||||
if self.is_interleaved {
|
||||
// this helps us catch component errors.
|
||||
self.set_upsampling()?;
|
||||
}
|
||||
if self.is_interleaved {
|
||||
mcu_width = self.mcu_x;
|
||||
mcu_height = self.mcu_y;
|
||||
} else {
|
||||
mcu_width = (self.info.width as usize + 7) / 8;
|
||||
mcu_height = (self.info.height as usize + 7) / 8;
|
||||
}
|
||||
if self.is_interleaved
|
||||
&& self.input_colorspace.num_components() > 1
|
||||
&& self.options.jpeg_get_out_colorspace().num_components() == 1
|
||||
&& (self.info.sample_ratio == SampleRatios::V
|
||||
|| self.info.sample_ratio == SampleRatios::HV)
|
||||
{
|
||||
// For a specific set of images, e.g interleaved,
|
||||
// when converting from YcbCr to grayscale, we need to
|
||||
// take into account mcu height since the MCU decoding needs to take
|
||||
// it into account for padding purposes and the post processor
|
||||
// parses two rows per mcu width.
|
||||
//
|
||||
// set coeff to be 2 to ensure that we increment two rows
|
||||
// for every mcu processed also
|
||||
mcu_height *= self.v_max;
|
||||
mcu_height /= self.h_max;
|
||||
self.coeff = 2;
|
||||
}
|
||||
|
||||
mcu_width *= 64;
|
||||
|
||||
for i in 0..self.input_colorspace.num_components() {
|
||||
let comp = &self.components[i];
|
||||
let len = mcu_width * comp.vertical_sample * comp.horizontal_sample * mcu_height;
|
||||
|
||||
block[i] = vec![0; len];
|
||||
}
|
||||
|
||||
let mut stream = BitStream::new_progressive(self.succ_low, self.spec_start, self.spec_end);
|
||||
|
||||
// there are multiple scans in the stream, this should resolve the first scan
|
||||
let result = self.parse_entropy_coded_data(&mut stream, &mut block);
|
||||
|
||||
if result.is_err() {
|
||||
return if self.options.strict_mode() {
|
||||
Err(result.err().unwrap())
|
||||
} else {
|
||||
error!("{}", result.err().unwrap());
|
||||
// Go process it and return as much as we can, exiting here
|
||||
return self.finish_progressive_decoding(&block, pixels);
|
||||
};
|
||||
}
|
||||
|
||||
// extract marker
|
||||
let mut marker = stream
|
||||
.marker
|
||||
.take()
|
||||
.ok_or(DecodeErrors::FormatStatic("Marker missing where expected"))?;
|
||||
|
||||
// if marker is EOI, we are done, otherwise continue scanning.
|
||||
//
|
||||
// In case we have a premature image, we print a warning or return
|
||||
// an error, depending on the strictness of the decoder, so there
|
||||
// is that logic to handle too
|
||||
'eoi: while marker != Marker::EOI {
|
||||
match marker {
|
||||
Marker::SOS => {
|
||||
parse_sos(self)?;
|
||||
|
||||
stream.update_progressive_params(
|
||||
self.succ_high,
|
||||
self.succ_low,
|
||||
self.spec_start,
|
||||
self.spec_end
|
||||
);
|
||||
// after every SOS, marker, parse data for that scan.
|
||||
let result = self.parse_entropy_coded_data(&mut stream, &mut block);
|
||||
|
||||
// Do not error out too fast, allows the decoder to continue as much as possible
|
||||
// even after errors
|
||||
if result.is_err() {
|
||||
return if self.options.strict_mode() {
|
||||
Err(result.err().unwrap())
|
||||
} else {
|
||||
error!("{}", result.err().unwrap());
|
||||
break 'eoi;
|
||||
};
|
||||
}
|
||||
// extract marker, might either indicate end of image or we continue
|
||||
// scanning(hence the continue statement to determine).
|
||||
match get_marker(&mut self.stream, &mut stream) {
|
||||
Ok(marker_n) => {
|
||||
marker = marker_n;
|
||||
seen_scans += 1;
|
||||
if seen_scans > self.options.jpeg_get_max_scans() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Too many scans, exceeded limit of {}",
|
||||
self.options.jpeg_get_max_scans()
|
||||
)));
|
||||
}
|
||||
|
||||
stream.reset();
|
||||
continue 'eoi;
|
||||
}
|
||||
Err(msg) => {
|
||||
if self.options.strict_mode() {
|
||||
return Err(msg);
|
||||
}
|
||||
error!("{:?}", msg);
|
||||
break 'eoi;
|
||||
}
|
||||
}
|
||||
}
|
||||
Marker::RST(_n) => {
|
||||
self.handle_rst(&mut stream)?;
|
||||
}
|
||||
_ => {
|
||||
self.parse_marker_inner(marker)?;
|
||||
}
|
||||
}
|
||||
|
||||
match get_marker(&mut self.stream, &mut stream) {
|
||||
Ok(marker_n) => {
|
||||
marker = marker_n;
|
||||
}
|
||||
Err(e) => {
|
||||
if self.options.strict_mode() {
|
||||
return Err(e);
|
||||
}
|
||||
error!("{}", e);
|
||||
// If we can't get the marker, just break away
|
||||
// allows us to decode some corrupt images
|
||||
// e.g https://github.com/etemesi254/zune-image/issues/294
|
||||
break 'eoi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.finish_progressive_decoding(&block, pixels)
|
||||
}
|
||||
|
||||
/// Reset progressive parameters
|
||||
fn reset_prog_params(&mut self, stream: &mut BitStream) {
|
||||
stream.reset();
|
||||
self.components.iter_mut().for_each(|x| x.dc_pred = 0);
|
||||
|
||||
// Also reset JPEG restart intervals
|
||||
self.todo = if self.restart_interval != 0 { self.restart_interval } else { usize::MAX };
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cast_sign_loss)]
|
||||
fn parse_entropy_coded_data(
|
||||
&mut self, stream: &mut BitStream, buffer: &mut [Vec<i16>; MAX_COMPONENTS]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
self.reset_prog_params(stream);
|
||||
|
||||
if usize::from(self.num_scans) > self.input_colorspace.num_components() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Number of scans {} cannot be greater than number of components, {}",
|
||||
self.num_scans,
|
||||
self.input_colorspace.num_components()
|
||||
)));
|
||||
}
|
||||
if self.num_scans == 1 {
|
||||
// Safety checks
|
||||
if self.spec_end != 0 && self.spec_start == 0 {
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Can't merge DC and AC corrupt jpeg"
|
||||
));
|
||||
}
|
||||
// non interleaved data, process one block at a time in trivial scanline order
|
||||
|
||||
let k = self.z_order[0];
|
||||
|
||||
if k >= self.components.len() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Cannot find component {k}, corrupt image"
|
||||
)));
|
||||
}
|
||||
// For non-interleaved scans, iterate over the component's actual data-unit grid.
|
||||
let component = &self.components[k];
|
||||
|
||||
let mcu_width = (self.info.width as usize * component.horizontal_sample).div_ceil(self.h_max * 8);
|
||||
let mcu_height = (self.info.height as usize * component.vertical_sample).div_ceil(self.v_max * 8);
|
||||
|
||||
for i in 0..mcu_height {
|
||||
for j in 0..mcu_width {
|
||||
if self.spec_start != 0 && self.succ_high == 0 && stream.eob_run > 0 {
|
||||
// handle EOB runs here.
|
||||
stream.eob_run -= 1;
|
||||
} else {
|
||||
let start = 64 * (j + i * (self.components[k].width_stride / 8));
|
||||
|
||||
let data: &mut [i16; 64] = buffer
|
||||
.get_mut(k)
|
||||
.unwrap()
|
||||
.get_mut(start..start + 64)
|
||||
.ok_or(DecodeErrors::FormatStatic("Slice to Small"))?
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
if self.spec_start == 0 {
|
||||
let pos = self.components[k].dc_huff_table & (MAX_COMPONENTS - 1);
|
||||
let dc_table = self
|
||||
.dc_huffman_tables
|
||||
.get(pos)
|
||||
.ok_or(DecodeErrors::FormatStatic(
|
||||
"No huffman table for DC component"
|
||||
))?
|
||||
.as_ref()
|
||||
.ok_or(DecodeErrors::FormatStatic(
|
||||
"Huffman table at index {} not initialized"
|
||||
))?;
|
||||
|
||||
let dc_pred = &mut self.components[k].dc_pred;
|
||||
|
||||
if self.succ_high == 0 {
|
||||
// first scan for this mcu
|
||||
stream.decode_prog_dc_first(
|
||||
&mut self.stream,
|
||||
dc_table,
|
||||
&mut data[0],
|
||||
dc_pred
|
||||
)?;
|
||||
} else {
|
||||
// refining scans for this MCU
|
||||
stream.decode_prog_dc_refine(&mut self.stream, &mut data[0])?;
|
||||
}
|
||||
} else {
|
||||
let pos = self.components[k].ac_huff_table;
|
||||
let ac_table = self
|
||||
.ac_huffman_tables
|
||||
.get(pos)
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::Format(format!(
|
||||
"No huffman table for component:{pos}"
|
||||
))
|
||||
})?
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::Format(format!(
|
||||
"Huffman table at index {pos} not initialized"
|
||||
))
|
||||
})?;
|
||||
|
||||
if self.succ_high == 0 {
|
||||
debug_assert!(stream.eob_run == 0, "EOB run is not zero");
|
||||
|
||||
stream.decode_mcu_ac_first(&mut self.stream, ac_table, data)?;
|
||||
} else {
|
||||
// refinement scan
|
||||
stream.decode_mcu_ac_refine(&mut self.stream, ac_table, data)?;
|
||||
}
|
||||
// Check for a marker.
|
||||
// It can appear in stream CC https://github.com/etemesi254/zune-image/issues/300
|
||||
// if let Some(marker) = stream.marker.take() {
|
||||
// self.parse_marker_inner(marker)?;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// + EOB and investigate effect.
|
||||
self.todo -= 1;
|
||||
|
||||
self.handle_rst_main(stream)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.spec_end != 0 {
|
||||
return Err(DecodeErrors::HuffmanDecode(
|
||||
"Can't merge dc and AC corrupt jpeg".to_string()
|
||||
));
|
||||
}
|
||||
// process scan n elements in order
|
||||
|
||||
// Do the error checking with allocs here.
|
||||
// Make the one in the inner loop free of allocations.
|
||||
for k in 0..self.num_scans {
|
||||
let n = self.z_order[k as usize];
|
||||
|
||||
if n >= self.components.len() {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Cannot find component {n}, corrupt image"
|
||||
)));
|
||||
}
|
||||
|
||||
let component = &mut self.components[n];
|
||||
let _ = self
|
||||
.dc_huffman_tables
|
||||
.get(component.dc_huff_table)
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::Format(format!(
|
||||
"No huffman table for component:{}",
|
||||
component.dc_huff_table
|
||||
))
|
||||
})?
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::Format(format!(
|
||||
"Huffman table at index {} not initialized",
|
||||
component.dc_huff_table
|
||||
))
|
||||
})?;
|
||||
}
|
||||
// Interleaved scan
|
||||
|
||||
// Components shall not be interleaved in progressive mode, except for
|
||||
// the DC coefficients in the first scan for each component of a progressive frame.
|
||||
for i in 0..self.mcu_y {
|
||||
for j in 0..self.mcu_x {
|
||||
// process scan n elements in order
|
||||
for k in 0..self.num_scans {
|
||||
let n = self.z_order[k as usize];
|
||||
let component = &mut self.components[n];
|
||||
let huff_table = self
|
||||
.dc_huffman_tables
|
||||
.get(component.dc_huff_table)
|
||||
.ok_or(DecodeErrors::FormatStatic("No huffman table for component"))?
|
||||
.as_ref()
|
||||
.ok_or(DecodeErrors::FormatStatic(
|
||||
"Huffman table at index not initialized"
|
||||
))?;
|
||||
|
||||
for v_samp in 0..component.vertical_sample {
|
||||
for h_samp in 0..component.horizontal_sample {
|
||||
let x2 = j * component.horizontal_sample + h_samp;
|
||||
let y2 = i * component.vertical_sample + v_samp;
|
||||
let position = 64 * (x2 + y2 * component.width_stride / 8);
|
||||
let buf_n = &mut buffer[n];
|
||||
|
||||
let Some(data) = &mut buf_n.get_mut(position) else {
|
||||
// TODO: (CAE), this is another weird sub-sampling bug, so on fix
|
||||
// remove this
|
||||
return Err(DecodeErrors::FormatStatic("Invalid image"));
|
||||
};
|
||||
|
||||
if self.succ_high == 0 {
|
||||
stream.decode_prog_dc_first(
|
||||
&mut self.stream,
|
||||
huff_table,
|
||||
data,
|
||||
&mut component.dc_pred
|
||||
)?;
|
||||
} else {
|
||||
stream.decode_prog_dc_refine(&mut self.stream, data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We want wrapping subtraction here because it means
|
||||
// we get a higher number in the case this underflows
|
||||
self.todo -= 1;
|
||||
// after every scan that's a mcu, count down restart markers.
|
||||
self.handle_rst_main(stream)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub(crate) fn handle_rst_main(&mut self, stream: &mut BitStream) -> Result<(), DecodeErrors> {
|
||||
if self.todo == 0 {
|
||||
stream.refill(&mut self.stream)?;
|
||||
}
|
||||
|
||||
if self.todo == 0
|
||||
&& self.restart_interval != 0
|
||||
&& stream.marker.is_none()
|
||||
&& !stream.seen_eoi
|
||||
{
|
||||
// if no marker and we are to reset RST, look for the marker, this matches
|
||||
// libjpeg-turbo behaviour and allows us to decode images in
|
||||
// https://github.com/etemesi254/zune-image/issues/261
|
||||
let _start = self.stream.position()?;
|
||||
// skip bytes until we find marker
|
||||
let marker = get_marker(&mut self.stream, stream);
|
||||
|
||||
// In some images, the RST marker on the last section may not be available
|
||||
// as it is maybe stopped by an EOI marker, see in the case of https://github.com/etemesi254/zune-image/issues/292
|
||||
// what happened was that we would go looking for the RST marker exhausting all the data
|
||||
// in the image and this would return an error, so for now
|
||||
// translate it to a warning, but return the image decoded up
|
||||
// until that point
|
||||
if let Ok(marker) = marker {
|
||||
let _end = self.stream.position()?;
|
||||
stream.marker = Some(marker);
|
||||
// NB some warnings may be false positives.
|
||||
warn!(
|
||||
"{} Extraneous bytes before marker {:?}",
|
||||
_end - _start,
|
||||
marker
|
||||
);
|
||||
} else {
|
||||
warn!("RST marker was not found, where expected, image may be garbled")
|
||||
}
|
||||
}
|
||||
if self.todo == 0 {
|
||||
self.handle_rst(stream)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::needless_range_loop, clippy::cast_sign_loss)]
|
||||
fn finish_progressive_decoding(
|
||||
&mut self, block: &[Vec<i16>; MAX_COMPONENTS], pixels: &mut [u8]
|
||||
) -> Result<(), DecodeErrors> {
|
||||
// This function is complicated because we need to replicate
|
||||
// the function in mcu.rs
|
||||
//
|
||||
// The advantage is that we do very little allocation and very lot
|
||||
// channel reusing.
|
||||
// The trick is to notice that we repeat the same procedure per MCU
|
||||
// width.
|
||||
//
|
||||
// So we can set it up that we only allocate temporary storage large enough
|
||||
// to store a single mcu width, then reuse it per invocation.
|
||||
//
|
||||
// This is advantageous to us.
|
||||
//
|
||||
// Remember we need to have the whole MCU buffer so we store 3 unprocessed
|
||||
// channels in memory, and then we allocate the whole output buffer in memory, both of
|
||||
// which are huge.
|
||||
//
|
||||
//
|
||||
|
||||
let mcu_height = if self.is_interleaved {
|
||||
self.mcu_y
|
||||
} else {
|
||||
// For non-interleaved images( (1*1) subsampling)
|
||||
// number of MCU's are the widths (+7 to account for paddings) divided by 8.
|
||||
self.info.height.div_ceil(8) as usize
|
||||
};
|
||||
|
||||
// Size of our output image(width*height)
|
||||
let is_hv = usize::from(self.is_interleaved);
|
||||
let upsampler_scratch_size = is_hv * self.components[0].width_stride;
|
||||
let width = usize::from(self.info.width);
|
||||
let padded_width = calculate_padded_width(width, self.info.sample_ratio);
|
||||
|
||||
let mut upsampler_scratch_space = vec![0; upsampler_scratch_size];
|
||||
let mut tmp = [0_i32; DCT_BLOCK];
|
||||
|
||||
for (pos, comp) in self.components.iter_mut().enumerate() {
|
||||
// Allocate only needed components.
|
||||
//
|
||||
// For special colorspaces i.e YCCK and CMYK, just allocate all of the needed
|
||||
// components.
|
||||
if min(
|
||||
self.options.jpeg_get_out_colorspace().num_components() - 1,
|
||||
pos
|
||||
) == pos
|
||||
|| self.input_colorspace == ColorSpace::YCCK
|
||||
|| self.input_colorspace == ColorSpace::CMYK
|
||||
{
|
||||
// allocate enough space to hold a whole MCU width
|
||||
// this means we should take into account sampling ratios
|
||||
// `*8` is because each MCU spans 8 widths.
|
||||
let len = comp.width_stride * comp.vertical_sample * 8;
|
||||
|
||||
comp.needed = true;
|
||||
comp.raw_coeff = vec![0; len];
|
||||
} else {
|
||||
comp.needed = false;
|
||||
}
|
||||
}
|
||||
|
||||
let mut pixels_written = 0;
|
||||
|
||||
// dequantize, idct and color convert.
|
||||
for i in 0..mcu_height {
|
||||
'component: for (position, component) in &mut self.components.iter_mut().enumerate() {
|
||||
if !component.needed {
|
||||
continue 'component;
|
||||
}
|
||||
let qt_table = &component.quantization_table;
|
||||
|
||||
// step is the number of pixels this iteration wil be handling
|
||||
// Given by the number of mcu's height and the length of the component block
|
||||
// Since the component block contains the whole channel as raw pixels
|
||||
// we this evenly divides the pixels into MCU blocks
|
||||
//
|
||||
// For interleaved images, this gives us the exact pixels comprising a whole MCU
|
||||
// block
|
||||
let step = block[position].len() / mcu_height;
|
||||
// where we will be reading our pixels from.
|
||||
let start = i * step;
|
||||
|
||||
let slice = &block[position][start..start + step];
|
||||
|
||||
let temp_channel = &mut component.raw_coeff;
|
||||
|
||||
// The next logical step is to iterate width wise.
|
||||
// To figure out how many pixels we iterate by we use effective pixels
|
||||
// Given to us by component.x
|
||||
// iterate per effective pixels.
|
||||
let mcu_x = component.width_stride / 8;
|
||||
|
||||
// iterate per every vertical sample.
|
||||
for k in 0..component.vertical_sample {
|
||||
for j in 0..mcu_x {
|
||||
// after writing a single stride, we need to skip 8 rows.
|
||||
// This does the row calculation
|
||||
let width_stride = k * 8 * component.width_stride;
|
||||
let start = j * 64 + width_stride;
|
||||
|
||||
// See https://github.com/etemesi254/zune-image/issues/262 sample 3.
|
||||
let Some(qt_slice) = slice.get(start..start + 64) else {
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Invalid slice , would panic, invalid image"
|
||||
));
|
||||
};
|
||||
// dequantize
|
||||
for ((x, out), qt_val) in
|
||||
qt_slice.iter().zip(tmp.iter_mut()).zip(qt_table.iter())
|
||||
{
|
||||
*out = i32::from(*x) * qt_val;
|
||||
}
|
||||
// determine where to write.
|
||||
let sl = &mut temp_channel[component.idct_pos..];
|
||||
|
||||
component.idct_pos += 8;
|
||||
// tmp now contains a dequantized block so idct it
|
||||
(self.idct_func)(&mut tmp, sl, component.width_stride);
|
||||
}
|
||||
// after every write of 8, skip 7 since idct write stride wise 8 times.
|
||||
//
|
||||
// Remember each MCU is 8x8 block, so each idct will write 8 strides into
|
||||
// sl
|
||||
//
|
||||
// and component.idct_pos is one stride long
|
||||
component.idct_pos += 7 * component.width_stride;
|
||||
}
|
||||
component.idct_pos = 0;
|
||||
}
|
||||
|
||||
// process that width up until it's impossible
|
||||
self.post_process(
|
||||
pixels,
|
||||
i,
|
||||
mcu_height,
|
||||
width,
|
||||
padded_width,
|
||||
&mut pixels_written,
|
||||
&mut upsampler_scratch_space
|
||||
)?;
|
||||
}
|
||||
|
||||
debug!("Finished decoding image");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
pub(crate) fn reset_params(&mut self) {
|
||||
/*
|
||||
Apparently, grayscale images which can be down sampled exists, which is weird in the sense
|
||||
that it has one component Y, which is not usually down sampled.
|
||||
|
||||
This means some calculations will be wrong, so for that we explicitly reset params
|
||||
for such occurrences, warn and reset the image info to appear as if it were
|
||||
a non-sampled image to ensure decoding works
|
||||
*/
|
||||
self.h_max = 1;
|
||||
self.v_max = 1;
|
||||
self.info.sample_ratio = SampleRatios::None;
|
||||
self.is_interleaved = false;
|
||||
self.components[0].vertical_sample = 1;
|
||||
self.components[0].width_stride = (((self.info.width as usize) + 7) / 8) * 8;
|
||||
self.components[0].horizontal_sample = 1;
|
||||
}
|
||||
}
|
||||
|
||||
///Get a marker from the bit-stream.
|
||||
///
|
||||
/// This reads until it gets a marker or end of file is encountered
|
||||
pub fn get_marker<T>(
|
||||
reader: &mut ZReader<T>, stream: &mut BitStream
|
||||
) -> Result<Marker, DecodeErrors>
|
||||
where
|
||||
T: ZByteReaderTrait
|
||||
{
|
||||
if let Some(marker) = stream.marker {
|
||||
stream.marker = None;
|
||||
return Ok(marker);
|
||||
}
|
||||
|
||||
// read until we get a marker
|
||||
|
||||
while !reader.eof()? {
|
||||
let marker = reader.read_u8_err()?;
|
||||
|
||||
if marker == 255 {
|
||||
let mut r = reader.read_u8_err()?;
|
||||
// 0xFF 0XFF(some images may be like that)
|
||||
while r == 0xFF {
|
||||
r = reader.read_u8_err()?;
|
||||
}
|
||||
|
||||
if r != 0 {
|
||||
return Marker::from_u8(r)
|
||||
.ok_or_else(|| DecodeErrors::Format(format!("Unknown marker 0xFF{r:X}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(DecodeErrors::ExhaustedData);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests{
|
||||
use zune_core::bytestream::ZCursor;
|
||||
use crate::JpegDecoder;
|
||||
|
||||
#[test]
|
||||
fn make_test(){
|
||||
let img = "/Users/etemesi/Downloads/wrong_sampling.jpeg";
|
||||
let data = ZCursor::new([255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 0, 2, 0, 28, 0, 28, 0, 0, 255, 219, 0, 67, 0, 40, 28, 30, 20, 30, 25, 40, 35, 33, 35, 45, 43, 40, 48, 60, 100, 65, 60, 55, 55, 60, 123, 88, 93, 65, 100, 145, 128, 153, 150, 143, 128, 140, 138, 160, 180, 230, 195, 160, 170, 218, 173, 138, 140, 200, 255, 203, 218, 255, 238, 245, 255, 101, 0, 62, 8, 255, 255, 250, 255, 230, 253, 255, 17, 255, 219, 0, 67, 1, 43, 45, 45, 42, 60, 48, 60, 118, 65, 65, 118, 248, 165, 140, 165, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 241, 255, 255, 255, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 255, 192, 0, 17, 8, 0, 32, 0, 32, 3, 2, 17, 0, 1, 34, 1, 3, 17, 1, 255, 196, 0, 24, 0, 1, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 3, 0, 1, 4, 255, 196, 0, 37, 16, 0, 2, 2, 1, 4, 1, 3, 5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 17, 0, 4, 18, 33, 48, 34, 65, 81, 113, 19, 20, 51, 97, 161, 255, 196, 0, 22, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 255, 196, 0, 26, 17, 1, 0, 2, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 17, 18, 38, 65, 255, 218, 0, 12, 3, 1, 0, 2, 17, 3, 17, 0, 63, 0, 175, 119, 49, 197, 184, 2, 0, 0, 0, 16, 13, 129, 103, 161, 102, 178, 115, 125, 202, 68, 236, 173, 25, 42, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 38, 0, 0, 0, 0, 250, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 67, 1, 43, 45, 45, 60, 48, 60, 118, 65, 65, 118, 248, 165, 140, 165, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 241, 255, 255, 255, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 255, 192, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 255, 192, 0, 17, 8, 0, 32, 0, 32, 3, 1, 34, 0, 2, 17, 1, 3, 17, 1, 255, 196, 0, 24, 0, 1, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 126, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 198]);
|
||||
let mut decoder = JpegDecoder::new(data);
|
||||
decoder.decode().unwrap();
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
+485
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//!Miscellaneous stuff
|
||||
#![allow(dead_code)]
|
||||
|
||||
use alloc::format;
|
||||
use core::cmp::max;
|
||||
use core::fmt;
|
||||
use core::num::NonZeroU32;
|
||||
|
||||
use zune_core::bytestream::ZByteReaderTrait;
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
use zune_core::log::{trace, warn};
|
||||
|
||||
use crate::components::{ComponentID, SampleRatios};
|
||||
use crate::errors::DecodeErrors;
|
||||
use crate::huffman::HuffmanTable;
|
||||
use crate::JpegDecoder;
|
||||
|
||||
/// Start of baseline DCT Huffman coding
|
||||
|
||||
pub const START_OF_FRAME_BASE: u16 = 0xffc0;
|
||||
|
||||
/// Start of another frame
|
||||
|
||||
pub const START_OF_FRAME_EXT_SEQ: u16 = 0xffc1;
|
||||
|
||||
/// Start of progressive DCT encoding
|
||||
|
||||
pub const START_OF_FRAME_PROG_DCT: u16 = 0xffc2;
|
||||
|
||||
/// Start of Lossless sequential Huffman coding
|
||||
|
||||
pub const START_OF_FRAME_LOS_SEQ: u16 = 0xffc3;
|
||||
|
||||
/// Start of extended sequential DCT arithmetic coding
|
||||
|
||||
pub const START_OF_FRAME_EXT_AR: u16 = 0xffc9;
|
||||
|
||||
/// Start of Progressive DCT arithmetic coding
|
||||
|
||||
pub const START_OF_FRAME_PROG_DCT_AR: u16 = 0xffca;
|
||||
|
||||
/// Start of Lossless sequential Arithmetic coding
|
||||
|
||||
pub const START_OF_FRAME_LOS_SEQ_AR: u16 = 0xffcb;
|
||||
|
||||
/// Undo run length encoding of coefficients by placing them in natural order
|
||||
///
|
||||
/// This is an index from position-in-bitstream to position-in-row-major-order.
|
||||
#[rustfmt::skip]
|
||||
pub const UN_ZIGZAG: [usize; 64 + 16] = [
|
||||
0, 1, 8, 16, 9, 2, 3, 10,
|
||||
17, 24, 32, 25, 18, 11, 4, 5,
|
||||
12, 19, 26, 33, 40, 48, 41, 34,
|
||||
27, 20, 13, 6, 7, 14, 21, 28,
|
||||
35, 42, 49, 56, 57, 50, 43, 36,
|
||||
29, 22, 15, 23, 30, 37, 44, 51,
|
||||
58, 59, 52, 45, 38, 31, 39, 46,
|
||||
53, 60, 61, 54, 47, 55, 62, 63,
|
||||
// Prevent overflowing
|
||||
63, 63, 63, 63, 63, 63, 63, 63,
|
||||
63, 63, 63, 63, 63, 63, 63, 63
|
||||
];
|
||||
|
||||
/// Align data to a 16 byte boundary
|
||||
#[repr(align(16))]
|
||||
#[derive(Clone)]
|
||||
|
||||
pub struct Aligned16<T: ?Sized>(pub T);
|
||||
|
||||
impl<T> Default for Aligned16<T>
|
||||
where
|
||||
T: Default
|
||||
{
|
||||
fn default() -> Self {
|
||||
Aligned16(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Align data to a 32 byte boundary
|
||||
#[repr(align(32))]
|
||||
#[derive(Clone)]
|
||||
pub struct Aligned32<T: ?Sized>(pub T);
|
||||
|
||||
impl<T> Default for Aligned32<T>
|
||||
where
|
||||
T: Default
|
||||
{
|
||||
fn default() -> Self {
|
||||
Aligned32(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Markers that identify different Start of Image markers
|
||||
/// They identify the type of encoding and whether the file use lossy(DCT) or
|
||||
/// lossless compression and whether we use Huffman or arithmetic coding schemes
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum SOFMarkers {
|
||||
/// Baseline DCT markers
|
||||
BaselineDct,
|
||||
/// SOF_1 Extended sequential DCT,Huffman coding
|
||||
ExtendedSequentialHuffman,
|
||||
/// Progressive DCT, Huffman coding
|
||||
ProgressiveDctHuffman,
|
||||
/// Lossless (sequential), huffman coding,
|
||||
LosslessHuffman,
|
||||
/// Extended sequential DEC, arithmetic coding
|
||||
ExtendedSequentialDctArithmetic,
|
||||
/// Progressive DCT, arithmetic coding,
|
||||
ProgressiveDctArithmetic,
|
||||
/// Lossless ( sequential), arithmetic coding
|
||||
LosslessArithmetic
|
||||
}
|
||||
|
||||
impl Default for SOFMarkers {
|
||||
fn default() -> Self {
|
||||
Self::BaselineDct
|
||||
}
|
||||
}
|
||||
|
||||
impl SOFMarkers {
|
||||
/// Check if a certain marker is sequential DCT or not
|
||||
|
||||
pub fn is_sequential_dct(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::BaselineDct
|
||||
| Self::ExtendedSequentialHuffman
|
||||
| Self::ExtendedSequentialDctArithmetic
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if a marker is a Lossles type or not
|
||||
|
||||
pub fn is_lossless(self) -> bool {
|
||||
matches!(self, Self::LosslessHuffman | Self::LosslessArithmetic)
|
||||
}
|
||||
|
||||
/// Check whether a marker is a progressive marker or not
|
||||
|
||||
pub fn is_progressive(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ProgressiveDctHuffman | Self::ProgressiveDctArithmetic
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a marker from an integer
|
||||
|
||||
pub fn from_int(int: u16) -> Option<SOFMarkers> {
|
||||
match int {
|
||||
START_OF_FRAME_BASE => Some(Self::BaselineDct),
|
||||
START_OF_FRAME_PROG_DCT => Some(Self::ProgressiveDctHuffman),
|
||||
START_OF_FRAME_PROG_DCT_AR => Some(Self::ProgressiveDctArithmetic),
|
||||
START_OF_FRAME_LOS_SEQ => Some(Self::LosslessHuffman),
|
||||
START_OF_FRAME_LOS_SEQ_AR => Some(Self::LosslessArithmetic),
|
||||
START_OF_FRAME_EXT_SEQ => Some(Self::ExtendedSequentialHuffman),
|
||||
START_OF_FRAME_EXT_AR => Some(Self::ExtendedSequentialDctArithmetic),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SOFMarkers {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
Self::BaselineDct => write!(f, "Baseline DCT"),
|
||||
Self::ExtendedSequentialHuffman => {
|
||||
write!(f, "Extended sequential DCT, Huffman Coding")
|
||||
}
|
||||
Self::ProgressiveDctHuffman => write!(f, "Progressive DCT,Huffman Encoding"),
|
||||
Self::LosslessHuffman => write!(f, "Lossless (sequential) Huffman encoding"),
|
||||
Self::ExtendedSequentialDctArithmetic => {
|
||||
write!(f, "Extended sequential DCT, arithmetic coding")
|
||||
}
|
||||
Self::ProgressiveDctArithmetic => write!(f, "Progressive DCT, arithmetic coding"),
|
||||
Self::LosslessArithmetic => write!(f, "Lossless (sequential) arithmetic coding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up component parameters.
|
||||
///
|
||||
/// This modifies the components in place setting up details needed by other
|
||||
/// parts fo the decoder.
|
||||
pub(crate) fn setup_component_params<T: ZByteReaderTrait>(
|
||||
img: &mut JpegDecoder<T>
|
||||
) -> Result<(), DecodeErrors> {
|
||||
let img_width = img.width();
|
||||
let img_height = img.height();
|
||||
|
||||
// in case of adobe app14 being present, zero may indicate
|
||||
// either CMYK if components are 4 or RGB if components are 3,
|
||||
// see https://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
||||
// so since we may not know how many number of components
|
||||
// we have when decoding app14, we have to defer that check
|
||||
// until now.
|
||||
//
|
||||
// We know adobe app14 was present since it's the only one that can modify
|
||||
// input colorspace to be CMYK
|
||||
if img.components.len() == 3 && img.input_colorspace == ColorSpace::CMYK {
|
||||
img.input_colorspace = ColorSpace::RGB;
|
||||
}
|
||||
|
||||
for component in &mut img.components {
|
||||
// compute interleaved image info
|
||||
// h_max contains the maximum horizontal component
|
||||
img.h_max = max(img.h_max, component.horizontal_sample);
|
||||
// v_max contains the maximum vertical component
|
||||
img.v_max = max(img.v_max, component.vertical_sample);
|
||||
img.mcu_width = img.h_max * 8;
|
||||
img.mcu_height = img.v_max * 8;
|
||||
// Number of MCU's per width
|
||||
img.mcu_x = usize::from(img.info.width).div_ceil(img.mcu_width);
|
||||
// Number of MCU's per height
|
||||
img.mcu_y = usize::from(img.info.height).div_ceil(img.mcu_height);
|
||||
|
||||
if img.h_max != 1 || img.v_max != 1 {
|
||||
// interleaved images have horizontal and vertical sampling factors
|
||||
// not equal to 1.
|
||||
img.is_interleaved = true;
|
||||
}
|
||||
// Extract quantization tables from the arrays into components
|
||||
let qt_table = *img.qt_tables[component.quantization_table_number as usize]
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
DecodeErrors::DqtError(format!(
|
||||
"No quantization table for component {:?}",
|
||||
component.component_id
|
||||
))
|
||||
})?;
|
||||
|
||||
let x = (usize::from(img_width) * component.horizontal_sample + img.h_max - 1) / img.h_max;
|
||||
let y = (usize::from(img_height) * component.horizontal_sample + img.h_max - 1) / img.v_max;
|
||||
component.x = x;
|
||||
component.w2 = img.mcu_x * component.horizontal_sample * 8;
|
||||
// probably not needed. :)
|
||||
component.y = y;
|
||||
component.quantization_table = qt_table;
|
||||
// initially stride contains its horizontal sub-sampling
|
||||
component.width_stride *= img.mcu_x * 8;
|
||||
}
|
||||
{
|
||||
// Sampling factors are one thing that suck
|
||||
// this fixes a specific problem with images like
|
||||
//
|
||||
// (2 2) None
|
||||
// (2 1) H
|
||||
// (2 1) H
|
||||
//
|
||||
// The images exist in the wild, the images are not meant to exist
|
||||
// but they do, it's just an annoying horizontal sub-sampling that
|
||||
// I don't know why it exists.
|
||||
// But it does
|
||||
// So we try to cope with that.
|
||||
// I am not sure of how to explain how to fix it, but it involved a debugger
|
||||
// and to much coke(the legal one)
|
||||
//
|
||||
// If this wasn't present, self.upsample_dest would have the wrong length
|
||||
let mut handle_that_annoying_bug = false;
|
||||
|
||||
if let Some(y_component) = img
|
||||
.components
|
||||
.iter()
|
||||
.find(|c| c.component_id == ComponentID::Y)
|
||||
{
|
||||
if y_component.horizontal_sample == 2 || y_component.vertical_sample == 2 {
|
||||
handle_that_annoying_bug = true;
|
||||
}
|
||||
}
|
||||
if handle_that_annoying_bug {
|
||||
for comp in &mut img.components {
|
||||
if (comp.component_id != ComponentID::Y)
|
||||
&& (comp.horizontal_sample != 1 || comp.vertical_sample != 1)
|
||||
{
|
||||
comp.fix_an_annoying_bug = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if img.is_mjpeg {
|
||||
fill_default_mjpeg_tables(
|
||||
img.is_progressive,
|
||||
&mut img.dc_huffman_tables,
|
||||
&mut img.ac_huffman_tables
|
||||
);
|
||||
}
|
||||
|
||||
// check colorspace matches
|
||||
if img.input_colorspace.num_components() > img.components.len() {
|
||||
if img.input_colorspace == ColorSpace::YCCK {
|
||||
// Some images may have YCCK format (from adobe app14 segment) which is supposed to be 4 components
|
||||
// but only 3 components, see issue https://github.com/etemesi254/zune-image/issues/275
|
||||
// So this is the behaviour of other decoders
|
||||
// - stb_image: Treats it as YCbCr image
|
||||
// - libjpeg_turbo: Does not know how to parse YCCK images (transform 2 app14) so treats
|
||||
// it as YCbCr
|
||||
// So I will match that to match existing ones
|
||||
warn!("Treating YCCK colorspace as YCbCr as component length does not match");
|
||||
img.input_colorspace = ColorSpace::YCbCr
|
||||
} else {
|
||||
// Note, translated this to a warning to handle valid images of the sort
|
||||
// See https://github.com/etemesi254/zune-image/issues/288 where there
|
||||
// was a CMYK image with two components which would be decoded to 4 components
|
||||
// by the decoder.
|
||||
// So with a warning that becomes supported.
|
||||
//
|
||||
// djpeg fails to render an image from that also probably because it does not
|
||||
// understand the expected format.
|
||||
if !img.options.strict_mode() {
|
||||
warn!(
|
||||
"Expected {} number of components but found {}",
|
||||
img.input_colorspace.num_components(),
|
||||
img.components.len()
|
||||
);
|
||||
warn!("Defaulting to multisample to decode");
|
||||
|
||||
// N/B: We do not post process the color of such, treating it as multiband
|
||||
// is the best option since I am not aware of grayscale+alpha which is the most common
|
||||
// two band format in jpeg.
|
||||
if img.components.len() > 0 {
|
||||
img.input_colorspace = ColorSpace::MultiBand(
|
||||
NonZeroU32::new(img.components.len() as u32).unwrap()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let msg = format!(
|
||||
"Expected {} number of components but found {}",
|
||||
img.input_colorspace.num_components(),
|
||||
img.components.len()
|
||||
);
|
||||
|
||||
return Err(DecodeErrors::Format(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///Calculate number of fill bytes added to the end of a JPEG image
|
||||
/// to fill the image
|
||||
///
|
||||
/// JPEG usually inserts padding bytes if the image width cannot be evenly divided into
|
||||
/// 8 , 16 or 32 chunks depending on the sub sampling ratio. So given a sub-sampling ratio,
|
||||
/// and the actual width, this calculates the padded bytes that were added to the image
|
||||
///
|
||||
/// # Params
|
||||
/// -actual_width: Actual width of the image
|
||||
/// -sub_sample: Sub sampling factor of the image
|
||||
///
|
||||
/// # Returns
|
||||
/// The padded width, this is how long the width is for a particular image
|
||||
pub fn calculate_padded_width(actual_width: usize, sub_sample: SampleRatios) -> usize {
|
||||
match sub_sample {
|
||||
SampleRatios::None | SampleRatios::V => {
|
||||
// None+V sends one MCU row, so that's a simple calculation
|
||||
((actual_width + 7) / 8) * 8
|
||||
}
|
||||
SampleRatios::H | SampleRatios::HV => {
|
||||
// sends two rows, width can be expanded by up to 15 more bytes
|
||||
((actual_width + 15) / 16) * 16
|
||||
}
|
||||
SampleRatios::Generic(h, _) => {
|
||||
((actual_width + ((h * 8).saturating_sub(1))) / (h * 8)) * (h * 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.loc.gov/preservation/digital/formats/fdd/fdd000063.shtml
|
||||
// "Avery Lee, writing in the rec.video.desktop newsgroup in 2001, commented that "MJPEG, or at
|
||||
// least the MJPEG in AVIs having the MJPG fourcc, is restricted JPEG with a fixed -- and
|
||||
// *omitted* -- Huffman table. The JPEG must be YCbCr colorspace, it must be 4:2:2, and it must
|
||||
// use basic Huffman encoding, not arithmetic or progressive.... You can indeed extract the
|
||||
// MJPEG frames and decode them with a regular JPEG decoder, but you have to prepend the DHT
|
||||
// segment to them, or else the decoder won't have any idea how to decompress the data.
|
||||
// The exact table necessary is given in the OpenDML spec.""
|
||||
pub fn fill_default_mjpeg_tables(
|
||||
is_progressive: bool, dc_huffman_tables: &mut [Option<HuffmanTable>],
|
||||
ac_huffman_tables: &mut [Option<HuffmanTable>]
|
||||
) {
|
||||
// Section K.3.3
|
||||
trace!("Filling with default mjpeg tables");
|
||||
|
||||
if dc_huffman_tables[0].is_none() {
|
||||
// Table K.3
|
||||
dc_huffman_tables[0] = Some(
|
||||
HuffmanTable::new_unfilled(
|
||||
&[
|
||||
0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
],
|
||||
&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||
],
|
||||
true,
|
||||
is_progressive
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
if dc_huffman_tables[1].is_none() {
|
||||
// Table K.4
|
||||
dc_huffman_tables[1] = Some(
|
||||
HuffmanTable::new_unfilled(
|
||||
&[
|
||||
0x00, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
],
|
||||
&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||
],
|
||||
true,
|
||||
is_progressive
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
if ac_huffman_tables[0].is_none() {
|
||||
// Table K.5
|
||||
ac_huffman_tables[0] = Some(
|
||||
HuffmanTable::new_unfilled(
|
||||
&[
|
||||
0x00, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04,
|
||||
0x00, 0x00, 0x01, 0x7D
|
||||
],
|
||||
&[
|
||||
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13,
|
||||
0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42,
|
||||
0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A,
|
||||
0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35,
|
||||
0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
|
||||
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67,
|
||||
0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84,
|
||||
0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
|
||||
0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3,
|
||||
0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
|
||||
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1,
|
||||
0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
|
||||
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA
|
||||
],
|
||||
false,
|
||||
is_progressive
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
if ac_huffman_tables[1].is_none() {
|
||||
// Table K.6
|
||||
ac_huffman_tables[1] = Some(
|
||||
HuffmanTable::new_unfilled(
|
||||
&[
|
||||
0x00, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04,
|
||||
0x00, 0x01, 0x02, 0x77
|
||||
],
|
||||
&[
|
||||
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51,
|
||||
0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1,
|
||||
0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24,
|
||||
0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
||||
0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66,
|
||||
0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82,
|
||||
0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
|
||||
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA,
|
||||
0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
|
||||
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9,
|
||||
0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4,
|
||||
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA
|
||||
],
|
||||
false,
|
||||
is_progressive
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#[cfg(all(feature = "x86", any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
pub use crate::unsafe_utils_avx2::*;
|
||||
#[cfg(all(feature = "neon", target_arch = "aarch64"))]
|
||||
pub use crate::unsafe_utils_neon::*;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![cfg(all(feature = "x86", any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
//! This module provides unsafe ways to do some things
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
use core::arch::x86::*;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use core::arch::x86_64::*;
|
||||
use core::ops::{Add, AddAssign, Mul, MulAssign, Sub};
|
||||
|
||||
/// A copy of `_MM_SHUFFLE()` that doesn't require
|
||||
/// a nightly compiler
|
||||
#[inline]
|
||||
const fn shuffle(z: i32, y: i32, x: i32, w: i32) -> i32 {
|
||||
(z << 6) | (y << 4) | (x << 2) | w
|
||||
}
|
||||
|
||||
/// An abstraction of an AVX ymm register that
|
||||
///allows some things to not look ugly
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct YmmRegister {
|
||||
/// An AVX register
|
||||
pub(crate) mm256: __m256i
|
||||
}
|
||||
|
||||
impl Add for YmmRegister {
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
unsafe {
|
||||
return YmmRegister {
|
||||
mm256: _mm256_add_epi32(self.mm256, rhs.mm256)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<i32> for YmmRegister {
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: i32) -> Self::Output {
|
||||
unsafe {
|
||||
let tmp = _mm256_set1_epi32(rhs);
|
||||
|
||||
return YmmRegister {
|
||||
mm256: _mm256_add_epi32(self.mm256, tmp)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for YmmRegister {
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
unsafe {
|
||||
return YmmRegister {
|
||||
mm256: _mm256_sub_epi32(self.mm256, rhs.mm256)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for YmmRegister {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
unsafe {
|
||||
self.mm256 = _mm256_add_epi32(self.mm256, rhs.mm256);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<i32> for YmmRegister {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: i32) {
|
||||
unsafe {
|
||||
let tmp = _mm256_set1_epi32(rhs);
|
||||
|
||||
self.mm256 = _mm256_add_epi32(self.mm256, tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for YmmRegister {
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
unsafe {
|
||||
YmmRegister {
|
||||
mm256: _mm256_mullo_epi32(self.mm256, rhs.mm256)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<i32> for YmmRegister {
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: i32) -> Self::Output {
|
||||
unsafe {
|
||||
let tmp = _mm256_set1_epi32(rhs);
|
||||
|
||||
YmmRegister {
|
||||
mm256: _mm256_mullo_epi32(self.mm256, tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign for YmmRegister {
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, rhs: Self) {
|
||||
unsafe {
|
||||
self.mm256 = _mm256_mullo_epi32(self.mm256, rhs.mm256);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<i32> for YmmRegister {
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, rhs: i32) {
|
||||
unsafe {
|
||||
let tmp = _mm256_set1_epi32(rhs);
|
||||
|
||||
self.mm256 = _mm256_mullo_epi32(self.mm256, tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<__m256i> for YmmRegister {
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, rhs: __m256i) {
|
||||
unsafe {
|
||||
self.mm256 = _mm256_mullo_epi32(self.mm256, rhs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Reg = YmmRegister;
|
||||
|
||||
/// Transpose an array of 8 by 8 i32's using avx intrinsics
|
||||
///
|
||||
/// This was translated from [here](https://newbedev.com/transpose-an-8x8-float-using-avx-avx2)
|
||||
#[allow(unused_parens, clippy::too_many_arguments)]
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[inline]
|
||||
pub unsafe fn transpose(
|
||||
v0: &mut Reg, v1: &mut Reg, v2: &mut Reg, v3: &mut Reg, v4: &mut Reg, v5: &mut Reg,
|
||||
v6: &mut Reg, v7: &mut Reg
|
||||
) {
|
||||
macro_rules! merge_epi32 {
|
||||
($v0:tt,$v1:tt,$v2:tt,$v3:tt) => {
|
||||
let va = _mm256_permute4x64_epi64($v0, shuffle(3, 1, 2, 0));
|
||||
|
||||
let vb = _mm256_permute4x64_epi64($v1, shuffle(3, 1, 2, 0));
|
||||
|
||||
$v2 = _mm256_unpacklo_epi32(va, vb);
|
||||
|
||||
$v3 = _mm256_unpackhi_epi32(va, vb);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! merge_epi64 {
|
||||
($v0:tt,$v1:tt,$v2:tt,$v3:tt) => {
|
||||
let va = _mm256_permute4x64_epi64($v0, shuffle(3, 1, 2, 0));
|
||||
|
||||
let vb = _mm256_permute4x64_epi64($v1, shuffle(3, 1, 2, 0));
|
||||
|
||||
$v2 = _mm256_unpacklo_epi64(va, vb);
|
||||
|
||||
$v3 = _mm256_unpackhi_epi64(va, vb);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! merge_si128 {
|
||||
($v0:tt,$v1:tt,$v2:tt,$v3:tt) => {
|
||||
$v2 = _mm256_permute2x128_si256($v0, $v1, shuffle(0, 2, 0, 0));
|
||||
|
||||
$v3 = _mm256_permute2x128_si256($v0, $v1, shuffle(0, 3, 0, 1));
|
||||
};
|
||||
}
|
||||
|
||||
let (w0, w1, w2, w3, w4, w5, w6, w7);
|
||||
|
||||
merge_epi32!((v0.mm256), (v1.mm256), w0, w1);
|
||||
|
||||
merge_epi32!((v2.mm256), (v3.mm256), w2, w3);
|
||||
|
||||
merge_epi32!((v4.mm256), (v5.mm256), w4, w5);
|
||||
|
||||
merge_epi32!((v6.mm256), (v7.mm256), w6, w7);
|
||||
|
||||
let (x0, x1, x2, x3, x4, x5, x6, x7);
|
||||
|
||||
merge_epi64!(w0, w2, x0, x1);
|
||||
|
||||
merge_epi64!(w1, w3, x2, x3);
|
||||
|
||||
merge_epi64!(w4, w6, x4, x5);
|
||||
|
||||
merge_epi64!(w5, w7, x6, x7);
|
||||
|
||||
merge_si128!(x0, x4, (v0.mm256), (v1.mm256));
|
||||
|
||||
merge_si128!(x1, x5, (v2.mm256), (v3.mm256));
|
||||
|
||||
merge_si128!(x2, x6, (v4.mm256), (v5.mm256));
|
||||
|
||||
merge_si128!(x3, x7, (v6.mm256), (v7.mm256));
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#![cfg(all(feature = "neon", target_arch = "aarch64"))]
|
||||
// TODO can this be extended to armv7
|
||||
|
||||
//! This module provides unsafe ways to do some things
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use core::arch::aarch64::*;
|
||||
use core::ops::{Add, AddAssign, BitOr, BitOrAssign, Mul, MulAssign, Sub};
|
||||
|
||||
pub type VecType = int32x4x2_t;
|
||||
|
||||
pub unsafe fn loadu(src: *const i32) -> VecType {
|
||||
vld1q_s32_x2(src as *const _)
|
||||
}
|
||||
|
||||
/// An abstraction of an AVX ymm register that
|
||||
///allows some things to not look ugly
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct YmmRegister {
|
||||
/// An AVX register
|
||||
pub(crate) mm256: VecType
|
||||
}
|
||||
|
||||
impl YmmRegister {
|
||||
#[inline]
|
||||
pub unsafe fn load(src: *const i32) -> Self {
|
||||
loadu(src).into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map2(self, other: Self, f: impl Fn(int32x4_t, int32x4_t) -> int32x4_t) -> Self {
|
||||
let m0 = f(self.mm256.0, other.mm256.0);
|
||||
let m1 = f(self.mm256.1, other.mm256.1);
|
||||
|
||||
YmmRegister {
|
||||
mm256: int32x4x2_t(m0, m1)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn all_zero(self) -> bool {
|
||||
unsafe {
|
||||
let both = vorrq_s32(self.mm256.0, self.mm256.1);
|
||||
let both_unsigned = vreinterpretq_u32_s32(both);
|
||||
0 == vmaxvq_u32(both_unsigned)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn const_shl<const N: i32>(self) -> Self {
|
||||
// Ensure that we logically shift left
|
||||
unsafe {
|
||||
let m0 = vreinterpretq_s32_u32(vshlq_n_u32::<N>(vreinterpretq_u32_s32(self.mm256.0)));
|
||||
let m1 = vreinterpretq_s32_u32(vshlq_n_u32::<N>(vreinterpretq_u32_s32(self.mm256.1)));
|
||||
|
||||
YmmRegister {
|
||||
mm256: int32x4x2_t(m0, m1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn const_shra<const N: i32>(self) -> Self {
|
||||
unsafe {
|
||||
let i0 = vshrq_n_s32::<N>(self.mm256.0);
|
||||
let i1 = vshrq_n_s32::<N>(self.mm256.1);
|
||||
|
||||
YmmRegister {
|
||||
mm256: int32x4x2_t(i0, i1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: T) -> Self::Output {
|
||||
let rhs = rhs.into();
|
||||
unsafe { self.map2(rhs, |a, b| vaddq_s32(a, b)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sub<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, rhs: T) -> Self::Output {
|
||||
let rhs = rhs.into();
|
||||
unsafe { self.map2(rhs, |a, b| vsubq_s32(a, b)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: T) {
|
||||
let rhs: Self = rhs.into();
|
||||
*self = *self + rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Mul<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: T) -> Self::Output {
|
||||
let rhs = rhs.into();
|
||||
unsafe { self.map2(rhs, |a, b| vmulq_s32(a, b)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MulAssign<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, rhs: T) {
|
||||
let rhs: Self = rhs.into();
|
||||
*self = *self * rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BitOr<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
type Output = YmmRegister;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: T) -> Self::Output {
|
||||
let rhs = rhs.into();
|
||||
unsafe { self.map2(rhs, |a, b| vorrq_s32(a, b)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BitOrAssign<T> for YmmRegister
|
||||
where
|
||||
T: Into<Self>
|
||||
{
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: T) {
|
||||
let rhs: Self = rhs.into();
|
||||
*self = *self | rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for YmmRegister {
|
||||
#[inline]
|
||||
fn from(val: i32) -> Self {
|
||||
unsafe {
|
||||
let dup = vdupq_n_s32(val);
|
||||
|
||||
YmmRegister {
|
||||
mm256: int32x4x2_t(dup, dup)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VecType> for YmmRegister {
|
||||
#[inline]
|
||||
fn from(mm256: VecType) -> Self {
|
||||
YmmRegister { mm256 }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
unsafe fn transpose4(
|
||||
v0: &mut int32x4_t, v1: &mut int32x4_t, v2: &mut int32x4_t, v3: &mut int32x4_t
|
||||
) {
|
||||
let w0 = vtrnq_s32(
|
||||
vreinterpretq_s32_s64(vtrn1q_s64(
|
||||
vreinterpretq_s64_s32(*v0),
|
||||
vreinterpretq_s64_s32(*v2)
|
||||
)),
|
||||
vreinterpretq_s32_s64(vtrn1q_s64(
|
||||
vreinterpretq_s64_s32(*v1),
|
||||
vreinterpretq_s64_s32(*v3)
|
||||
))
|
||||
);
|
||||
let w1 = vtrnq_s32(
|
||||
vreinterpretq_s32_s64(vtrn2q_s64(
|
||||
vreinterpretq_s64_s32(*v0),
|
||||
vreinterpretq_s64_s32(*v2)
|
||||
)),
|
||||
vreinterpretq_s32_s64(vtrn2q_s64(
|
||||
vreinterpretq_s64_s32(*v1),
|
||||
vreinterpretq_s64_s32(*v3)
|
||||
))
|
||||
);
|
||||
|
||||
*v0 = w0.0;
|
||||
*v1 = w0.1;
|
||||
*v2 = w1.0;
|
||||
*v3 = w1.1;
|
||||
}
|
||||
|
||||
/// Transpose an array of 8 by 8 i32
|
||||
/// Arm has dedicated interleave/transpose instructions
|
||||
/// we:
|
||||
/// 1. Transpose the upper left and lower right quadrants
|
||||
/// 2. Swap and transpose the upper right and lower left quadrants
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
pub unsafe fn transpose(
|
||||
v0: &mut YmmRegister, v1: &mut YmmRegister, v2: &mut YmmRegister, v3: &mut YmmRegister,
|
||||
v4: &mut YmmRegister, v5: &mut YmmRegister, v6: &mut YmmRegister, v7: &mut YmmRegister
|
||||
) {
|
||||
use core::mem::swap;
|
||||
|
||||
let ul0 = &mut v0.mm256.0;
|
||||
let ul1 = &mut v1.mm256.0;
|
||||
let ul2 = &mut v2.mm256.0;
|
||||
let ul3 = &mut v3.mm256.0;
|
||||
|
||||
let ur0 = &mut v0.mm256.1;
|
||||
let ur1 = &mut v1.mm256.1;
|
||||
let ur2 = &mut v2.mm256.1;
|
||||
let ur3 = &mut v3.mm256.1;
|
||||
|
||||
let ll0 = &mut v4.mm256.0;
|
||||
let ll1 = &mut v5.mm256.0;
|
||||
let ll2 = &mut v6.mm256.0;
|
||||
let ll3 = &mut v7.mm256.0;
|
||||
|
||||
let lr0 = &mut v4.mm256.1;
|
||||
let lr1 = &mut v5.mm256.1;
|
||||
let lr2 = &mut v6.mm256.1;
|
||||
let lr3 = &mut v7.mm256.1;
|
||||
|
||||
swap(ur0, ll0);
|
||||
swap(ur1, ll1);
|
||||
swap(ur2, ll2);
|
||||
swap(ur3, ll3);
|
||||
|
||||
transpose4(ul0, ul1, ul2, ul3);
|
||||
|
||||
transpose4(ur0, ur1, ur2, ur3);
|
||||
|
||||
transpose4(ll0, ll1, ll2, ll3);
|
||||
|
||||
transpose4(lr0, lr1, lr2, lr3);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transpose() {
|
||||
fn get_val(i: usize, j: usize) -> i32 {
|
||||
((i * 8) / (j + 1)) as i32
|
||||
}
|
||||
unsafe {
|
||||
let mut vals: [i32; 8 * 8] = [0; 8 * 8];
|
||||
|
||||
for i in 0..8 {
|
||||
for j in 0..8 {
|
||||
// some order-dependent value of i and j
|
||||
let value = get_val(i, j);
|
||||
vals[i * 8 + j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
let mut regs: [YmmRegister; 8] = core::mem::transmute(vals);
|
||||
let mut reg0 = regs[0];
|
||||
let mut reg1 = regs[1];
|
||||
let mut reg2 = regs[2];
|
||||
let mut reg3 = regs[3];
|
||||
let mut reg4 = regs[4];
|
||||
let mut reg5 = regs[5];
|
||||
let mut reg6 = regs[6];
|
||||
let mut reg7 = regs[7];
|
||||
|
||||
transpose(
|
||||
&mut reg0, &mut reg1, &mut reg2, &mut reg3, &mut reg4, &mut reg5, &mut reg6,
|
||||
&mut reg7
|
||||
);
|
||||
|
||||
regs[0] = reg0;
|
||||
regs[1] = reg1;
|
||||
regs[2] = reg2;
|
||||
regs[3] = reg3;
|
||||
regs[4] = reg4;
|
||||
regs[5] = reg5;
|
||||
regs[6] = reg6;
|
||||
regs[7] = reg7;
|
||||
|
||||
let vals_from_reg: [i32; 8 * 8] = core::mem::transmute(regs);
|
||||
|
||||
for i in 0..8 {
|
||||
for j in 0..i {
|
||||
let orig = vals[i * 8 + j];
|
||||
vals[i * 8 + j] = vals[j * 8 + i];
|
||||
vals[j * 8 + i] = orig;
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..8 {
|
||||
for j in 0..8 {
|
||||
assert_eq!(vals[j * 8 + i], get_val(i, j));
|
||||
assert_eq!(vals_from_reg[j * 8 + i], get_val(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(vals, vals_from_reg);
|
||||
}
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
//! Up-sampling routines
|
||||
//!
|
||||
//! The main upsampling method is a bi-linear interpolation or a "triangle
|
||||
//! filter " or libjpeg turbo `fancy_upsampling` which is a good compromise
|
||||
//! between speed and visual quality
|
||||
//!
|
||||
//! # The filter
|
||||
//! Each output pixel is made from `(3*A+B)/4` where A is the original
|
||||
//! pixel closer to the output and B is the one further.
|
||||
//!
|
||||
//! ```text
|
||||
//!+---+---+
|
||||
//! | A | B |
|
||||
//! +---+---+
|
||||
//! +-+-+-+-+
|
||||
//! | |P| | |
|
||||
//! +-+-+-+-+
|
||||
//! ```
|
||||
//!
|
||||
//! # Horizontal Bi-linear filter
|
||||
//! ```text
|
||||
//! |---+-----------+---+
|
||||
//! | | | |
|
||||
//! | A | |p1 | p2| | B |
|
||||
//! | | | |
|
||||
//! |---+-----------+---+
|
||||
//!
|
||||
//! ```
|
||||
//! For a horizontal bi-linear it's trivial to implement,
|
||||
//!
|
||||
//! `A` becomes the input closest to the output.
|
||||
//!
|
||||
//! `B` varies depending on output.
|
||||
//! - For odd positions, input is the `next` pixel after A
|
||||
//! - For even positions, input is the `previous` value before A.
|
||||
//!
|
||||
//! We iterate in a classic 1-D sliding window with a window of 3.
|
||||
//! For our sliding window approach, `A` is the 1st and `B` is either the 0th term or 2nd term
|
||||
//! depending on position we are writing.(see scalar code).
|
||||
//!
|
||||
//! For vector code see module sse for explanation.
|
||||
//!
|
||||
//! # Vertical bi-linear.
|
||||
//! Vertical up-sampling is a bit trickier.
|
||||
//!
|
||||
//! ```text
|
||||
//! +----+----+
|
||||
//! | A1 | A2 |
|
||||
//! +----+----+
|
||||
//! +----+----+
|
||||
//! | p1 | p2 |
|
||||
//! +----+-+--+
|
||||
//! +----+-+--+
|
||||
//! | p3 | p4 |
|
||||
//! +----+-+--+
|
||||
//! +----+----+
|
||||
//! | B1 | B2 |
|
||||
//! +----+----+
|
||||
//! ```
|
||||
//!
|
||||
//! For `p1`
|
||||
//! - `A1` is given a weight of `3` and `B1` is given a weight of 1.
|
||||
//!
|
||||
//! For `p3`
|
||||
//! - `B1` is given a weight of `3` and `A1` is given a weight of 1
|
||||
//!
|
||||
//! # Horizontal vertical downsampling/chroma quartering.
|
||||
//!
|
||||
//! Carry out a vertical filter in the first pass, then a horizontal filter in the second pass.
|
||||
#![allow(unreachable_code)]
|
||||
use zune_core::options::DecoderOptions;
|
||||
|
||||
use crate::components::UpSampler;
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
mod avx2;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
mod neon;
|
||||
#[cfg(feature = "portable_simd")]
|
||||
mod portable_simd;
|
||||
mod scalar;
|
||||
|
||||
// choose the best possible implementation for this platform
|
||||
#[allow(unused_variables)]
|
||||
pub fn choose_horizontal_samp_function(options: &DecoderOptions) -> UpSampler {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
if options.use_avx2() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_avx2()` only returns true if avx2 is supported.
|
||||
unsafe { avx2::upsample_horizontal_avx2(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
if options.use_neon() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_neon()` only returns true if neon is supported.
|
||||
unsafe { neon::upsample_horizontal_neon(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "portable_simd")]
|
||||
return portable_simd::upsample_horizontal_simd;
|
||||
return scalar::upsample_horizontal;
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn choose_hv_samp_function(options: &DecoderOptions) -> UpSampler {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
if options.use_avx2() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_avx2()` only returns true if avx2 is supported.
|
||||
unsafe { avx2::upsample_hv_avx2(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
if options.use_neon() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_neon()` only returns true if neon is supported.
|
||||
unsafe { neon::upsample_hv_neon(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "portable_simd")]
|
||||
return portable_simd::upsample_hv_simd;
|
||||
return scalar::upsample_hv;
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn choose_v_samp_function(options: &DecoderOptions) -> UpSampler {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
if options.use_avx2() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_avx2()` only returns true if avx2 is supported.
|
||||
unsafe { avx2::upsample_vertical_avx2(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
if options.use_neon() {
|
||||
return |a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: `options.use_neon()` only returns true if neon is supported.
|
||||
unsafe { neon::upsample_vertical_neon(a, b, c, d, e) }
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "portable_simd")]
|
||||
return portable_simd::upsample_vertical_simd;
|
||||
return scalar::upsample_vertical;
|
||||
}
|
||||
|
||||
/// Upsample nothing
|
||||
|
||||
pub fn upsample_no_op(
|
||||
_input: &[i16],
|
||||
_in_ref: &[i16],
|
||||
_in_near: &[i16],
|
||||
_scratch_space: &mut [i16],
|
||||
_output: &mut [i16],
|
||||
) {
|
||||
}
|
||||
|
||||
pub fn generic_sampler() -> UpSampler {
|
||||
scalar::upsample_generic
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "portable_simd")]
|
||||
mod portable_simd_impl {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn portable_simd_vertical() {
|
||||
_test_vertical(portable_simd::upsample_vertical_simd)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_simd_horizontal() {
|
||||
_test_horizontal(portable_simd::upsample_horizontal_simd)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_simd_hv() {
|
||||
_test_hv(portable_simd::upsample_hv_simd)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[cfg(feature = "x86")]
|
||||
#[cfg(target_feature = "avx2")]
|
||||
mod avx2_impl {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn avx2_vertical() {
|
||||
_test_vertical(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { avx2::upsample_vertical_avx2(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avx2_horizontal() {
|
||||
_test_horizontal(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { avx2::upsample_horizontal_avx2(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avx2_hv() {
|
||||
_test_hv(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { avx2::upsample_hv_avx2(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(feature = "neon")]
|
||||
#[cfg(target_feature = "neon")]
|
||||
mod neon_impl {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn neon_vertical() {
|
||||
_test_vertical(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { neon::upsample_vertical_neon(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_horizontal() {
|
||||
_test_horizontal(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { neon::upsample_horizontal_neon(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_hv() {
|
||||
_test_hv(|a: &[i16], b: &[i16], c: &[i16], d: &mut [i16], e: &mut [i16]| {
|
||||
// SAFETY: Test guarded behind `target_feature`
|
||||
unsafe { neon::upsample_hv_neon(a, b, c, d, e) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn _test_vertical(upsampler: UpSampler) {
|
||||
let width = 1024;
|
||||
let input: Vec<i16> = (0..width).map(|x| ((x + 10) % 256) as i16).collect();
|
||||
let in_near: Vec<i16> = (0..width).map(|x| ((x + 20) % 256) as i16).collect();
|
||||
let in_far: Vec<i16> = (0..width).map(|x| ((x + 30) % 256) as i16).collect();
|
||||
let mut scratch = vec![0i16; width];
|
||||
|
||||
let mut output_scalar = vec![0i16; width * 2];
|
||||
let mut output_fast = vec![0i16; width * 2];
|
||||
|
||||
scalar::upsample_vertical(&input, &in_near, &in_far, &mut scratch, &mut output_scalar);
|
||||
upsampler(&input, &in_near, &in_far, &mut scratch, &mut output_fast);
|
||||
|
||||
assert_eq!(output_scalar, output_fast);
|
||||
}
|
||||
|
||||
fn _test_horizontal(upsampler: UpSampler) {
|
||||
_test_horizontal_even_width(upsampler);
|
||||
_test_horizontal_odd_width(upsampler);
|
||||
}
|
||||
|
||||
fn _test_horizontal_even_width(upsampler: UpSampler) {
|
||||
let width = 1024;
|
||||
let input: Vec<i16> = (0..width).map(|x| ((x + 10) % 256) as i16).collect();
|
||||
|
||||
let mut scratch = vec![0i16; width];
|
||||
|
||||
let mut output_scalar = vec![0i16; width * 2];
|
||||
let mut output_fast = vec![0i16; width * 2];
|
||||
|
||||
scalar::upsample_horizontal(&input, &[], &[], &mut scratch, &mut output_scalar);
|
||||
upsampler(&input, &[], &[], &mut scratch, &mut output_fast);
|
||||
|
||||
assert_eq!(output_scalar, output_fast);
|
||||
}
|
||||
|
||||
fn _test_horizontal_odd_width(upsampler: UpSampler) {
|
||||
let width = 33;
|
||||
let input: Vec<i16> = (0..width).map(|x| ((x + 10) % 256) as i16).collect();
|
||||
let mut scratch = vec![0i16; width];
|
||||
let mut output_scalar = vec![0i16; width * 2];
|
||||
let mut output_fast = vec![0i16; width * 2];
|
||||
|
||||
scalar::upsample_horizontal(&input, &[], &[], &mut scratch, &mut output_scalar);
|
||||
upsampler(&input, &[], &[], &mut scratch, &mut output_fast);
|
||||
|
||||
assert_eq!(output_scalar, output_fast);
|
||||
}
|
||||
|
||||
fn _test_hv(upsampler: UpSampler) {
|
||||
let width = 512;
|
||||
let input: Vec<i16> = (0..width).map(|x| ((x + 10) % 256) as i16).collect();
|
||||
let in_near: Vec<i16> = (0..width).map(|x| ((x + 20) % 256) as i16).collect();
|
||||
let in_far: Vec<i16> = (0..width).map(|x| ((x + 30) % 256) as i16).collect();
|
||||
|
||||
// Output len is width * 4 for HV (vertical * 2, then horizontal * 2 for each row)
|
||||
// scratch is width * 2
|
||||
let mut scratch_scalar = vec![0i16; width * 2];
|
||||
let mut scratch_fast = vec![0i16; width * 2];
|
||||
let mut output_scalar = vec![0i16; width * 4];
|
||||
let mut output_fast = vec![0i16; width * 4];
|
||||
|
||||
scalar::upsample_hv(
|
||||
&input,
|
||||
&in_near,
|
||||
&in_far,
|
||||
&mut scratch_scalar,
|
||||
&mut output_scalar,
|
||||
);
|
||||
upsampler(
|
||||
&input,
|
||||
&in_near,
|
||||
&in_far,
|
||||
&mut scratch_fast,
|
||||
&mut output_fast,
|
||||
);
|
||||
|
||||
assert_eq!(output_scalar, output_fast);
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (c) 2025.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
use core::arch::x86::*;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use core::arch::x86_64::*;
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[target_feature(enable = "avx2")]
|
||||
pub unsafe fn upsample_horizontal_avx2(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert!(input.len() > 2);
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 18 {
|
||||
return super::scalar::upsample_horizontal(input, in_near, in_far, scratch, output);
|
||||
}
|
||||
|
||||
// First two pixels
|
||||
output[0] = input[0];
|
||||
output[1] = (input[0] * 3 + input[1] + 2) >> 2;
|
||||
|
||||
let v_three = _mm256_set1_epi16(3);
|
||||
let v_two = _mm256_set1_epi16(2);
|
||||
|
||||
let upsample16 = |input: &[i16; 18], output: &mut [i16; 32]| {
|
||||
let in_ptr = input.as_ptr();
|
||||
let out_ptr = output.as_mut_ptr();
|
||||
|
||||
// SAFETY: The input is 18 * 16 bit long, so the loads are safe.
|
||||
let (v_prev, v_curr, v_next) = unsafe {
|
||||
(
|
||||
_mm256_loadu_si256(in_ptr.add(0) as *const __m256i),
|
||||
_mm256_loadu_si256(in_ptr.add(1) as *const __m256i),
|
||||
_mm256_loadu_si256(in_ptr.add(2) as *const __m256i),
|
||||
)
|
||||
};
|
||||
|
||||
let v_common = _mm256_add_epi16(_mm256_mullo_epi16(v_curr, v_three), v_two);
|
||||
|
||||
let v_even = _mm256_srai_epi16(_mm256_add_epi16(v_common, v_prev), 2);
|
||||
let v_odd = _mm256_srai_epi16(_mm256_add_epi16(v_common, v_next), 2);
|
||||
|
||||
let v_res_1 = _mm256_unpacklo_epi16(v_even, v_odd);
|
||||
let v_res_2 = _mm256_unpackhi_epi16(v_even, v_odd);
|
||||
|
||||
let v_final_1 = _mm256_permute2x128_si256(v_res_1, v_res_2, 0x20);
|
||||
let v_final_2 = _mm256_permute2x128_si256(v_res_1, v_res_2, 0x31);
|
||||
|
||||
// SAFETY: The output is 32 * 16 bit long, so the stores are safe.
|
||||
unsafe {
|
||||
_mm256_storeu_si256(out_ptr as *mut __m256i, v_final_1);
|
||||
_mm256_storeu_si256(out_ptr.add(16) as *mut __m256i, v_final_2);
|
||||
}
|
||||
};
|
||||
|
||||
for (input, output) in input
|
||||
.windows(18)
|
||||
.step_by(16)
|
||||
.zip(output[2..].chunks_exact_mut(32))
|
||||
{
|
||||
upsample16(input.try_into().unwrap(), output.try_into().unwrap());
|
||||
}
|
||||
|
||||
// Upsample the remainder. This may have some overlap, but that's fine.
|
||||
if let Some(rest_input) = input.last_chunk::<18>() {
|
||||
let end = output.len() - 2;
|
||||
if let Some(rest_output) = output[..end].last_chunk_mut::<32>() {
|
||||
upsample16(rest_input, rest_output);
|
||||
}
|
||||
}
|
||||
|
||||
// Last two pixels.
|
||||
output[output.len() - 2] = (3 * input[len - 1] + input[len - 2] + 2) >> 2;
|
||||
output[output.len() - 1] = input[len - 1];
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[target_feature(enable = "avx2")]
|
||||
pub unsafe fn upsample_vertical_avx2(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert_eq!(in_near.len(), input.len());
|
||||
assert_eq!(in_far.len(), input.len());
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 16 {
|
||||
return super::scalar::upsample_vertical(input, in_near, in_far, scratch, output);
|
||||
}
|
||||
|
||||
let middle = output.len() / 2;
|
||||
let (out_top, out_bottom) = output.split_at_mut(middle);
|
||||
|
||||
let v_three = _mm256_set1_epi16(3);
|
||||
let v_two = _mm256_set1_epi16(2);
|
||||
|
||||
let upsample16 = |input: &[i16; 16],
|
||||
in_near: &[i16; 16],
|
||||
in_far: &[i16; 16],
|
||||
out_top: &mut [i16; 16],
|
||||
out_bottom: &mut [i16; 16]| {
|
||||
// SAFETY: Inputs are all 16 * 16 bit long, so the loads are safe.
|
||||
let (v_in, v_near, v_far) = unsafe {
|
||||
(
|
||||
_mm256_loadu_si256(input.as_ptr() as *const __m256i),
|
||||
_mm256_loadu_si256(in_near.as_ptr() as *const __m256i),
|
||||
_mm256_loadu_si256(in_far.as_ptr() as *const __m256i),
|
||||
)
|
||||
};
|
||||
|
||||
let v_common = _mm256_add_epi16(_mm256_mullo_epi16(v_in, v_three), v_two);
|
||||
|
||||
let v_out_top = _mm256_srai_epi16(_mm256_add_epi16(v_common, v_near), 2);
|
||||
let v_out_bottom = _mm256_srai_epi16(_mm256_add_epi16(v_common, v_far), 2);
|
||||
|
||||
// SAFETY: Outputs are 16 * 16 bit long, so the stores are safe.
|
||||
unsafe {
|
||||
_mm256_storeu_si256(out_top.as_mut_ptr() as *mut __m256i, v_out_top);
|
||||
_mm256_storeu_si256(out_bottom.as_mut_ptr() as *mut __m256i, v_out_bottom);
|
||||
}
|
||||
};
|
||||
|
||||
let chunks = input
|
||||
.chunks_exact(16)
|
||||
.zip(in_near.chunks_exact(16))
|
||||
.zip(in_far.chunks_exact(16))
|
||||
.zip(out_top.chunks_exact_mut(16))
|
||||
.zip(out_bottom.chunks_exact_mut(16));
|
||||
|
||||
for ((((input, in_near), in_far), out_top), out_bottom) in chunks {
|
||||
upsample16(
|
||||
input.try_into().unwrap(),
|
||||
in_near.try_into().unwrap(),
|
||||
in_far.try_into().unwrap(),
|
||||
out_top.try_into().unwrap(),
|
||||
out_bottom.try_into().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Upsample the remainder. This may have some overlap, but that's fine.
|
||||
// Edition upgrade will fix this nested awfulness.
|
||||
if let Some(rest) = input.last_chunk::<16>() {
|
||||
if let Some(rest_near) = in_near.last_chunk::<16>() {
|
||||
if let Some(rest_far) = in_far.last_chunk::<16>() {
|
||||
if let Some(mut rest_top) = out_top.last_chunk_mut::<16>() {
|
||||
if let Some(mut rest_bottom) = out_bottom.last_chunk_mut::<16>() {
|
||||
upsample16(rest, rest_near, rest_far, &mut rest_top, &mut rest_bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
#[target_feature(enable = "avx2")]
|
||||
pub unsafe fn upsample_hv_avx2(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch_space: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 4, output.len());
|
||||
assert!(input.len() * 2 <= scratch_space.len());
|
||||
let scratch_space = &mut scratch_space[..input.len() * 2];
|
||||
|
||||
|
||||
upsample_vertical_avx2(input, in_near, in_far, &mut [], scratch_space);
|
||||
|
||||
let scratch_half = scratch_space.len() / 2;
|
||||
let output_half = output.len() / 2;
|
||||
|
||||
let (scratch_top, scratch_bottom) = scratch_space.split_at_mut(scratch_half);
|
||||
let (out_top, out_bottom) = output.split_at_mut(output_half);
|
||||
|
||||
let mut t = [0];
|
||||
upsample_horizontal_avx2(scratch_top, &[], &[], &mut t, out_top);
|
||||
upsample_horizontal_avx2(scratch_bottom, &[], &[], &mut t, out_bottom);
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (c) 2025.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use core::arch::aarch64::*;
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[target_feature(enable = "neon")]
|
||||
pub unsafe fn upsample_horizontal_neon(
|
||||
input: &[i16], in_near: &[i16], in_far: &[i16], scratch: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert!(input.len() > 2);
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 10 {
|
||||
return super::scalar::upsample_horizontal(input, in_near, in_far, scratch, output);
|
||||
}
|
||||
|
||||
// First two pixels
|
||||
output[0] = input[0];
|
||||
output[1] = (input[0] * 3 + input[1] + 2) >> 2;
|
||||
|
||||
// SAFETY: NEON target feature is enabled on this function.
|
||||
let v_three = unsafe { vdupq_n_s16(3) };
|
||||
// SAFETY: NEON target feature is enabled on this function.
|
||||
let v_two = unsafe { vdupq_n_s16(2) };
|
||||
|
||||
let upsample8 = |input: &[i16; 10], output: &mut [i16; 16]| {
|
||||
let in_ptr = input.as_ptr();
|
||||
let out_ptr = output.as_mut_ptr();
|
||||
|
||||
// SAFETY: The input is 10 * 16 bit long, so the loads are safe.
|
||||
let (v_prev, v_curr, v_next) = unsafe {
|
||||
(
|
||||
vld1q_s16(in_ptr),
|
||||
vld1q_s16(in_ptr.add(1)),
|
||||
vld1q_s16(in_ptr.add(2))
|
||||
)
|
||||
};
|
||||
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_common = unsafe { vaddq_s16(vmulq_s16(v_curr, v_three), v_two) };
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_even = unsafe { vshrq_n_s16::<2>(vaddq_s16(v_common, v_prev)) };
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_odd = unsafe { vshrq_n_s16::<2>(vaddq_s16(v_common, v_next)) };
|
||||
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_res_1 = unsafe { vzip1q_s16(v_even, v_odd) };
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_res_2 = unsafe { vzip2q_s16(v_even, v_odd) };
|
||||
|
||||
// SAFETY: The output is 16 * 16 bit long, so the stores are safe.
|
||||
unsafe {
|
||||
vst1q_s16(out_ptr, v_res_1);
|
||||
vst1q_s16(out_ptr.add(8), v_res_2);
|
||||
}
|
||||
};
|
||||
|
||||
for (input, output) in input
|
||||
.windows(10)
|
||||
.step_by(8)
|
||||
.zip(output[2..].chunks_exact_mut(16))
|
||||
{
|
||||
upsample8(input.try_into().unwrap(), output.try_into().unwrap());
|
||||
}
|
||||
|
||||
// Upsample the remainder. This may have some overlap, but that's fine.
|
||||
if let Some(rest_input) = input.last_chunk::<10>() {
|
||||
let end = output.len() - 2;
|
||||
if let Some(rest_output) = output[..end].last_chunk_mut::<16>() {
|
||||
upsample8(rest_input, rest_output);
|
||||
}
|
||||
}
|
||||
|
||||
// Last two pixels.
|
||||
output[output.len() - 2] = (3 * input[len - 1] + input[len - 2] + 2) >> 2;
|
||||
output[output.len() - 1] = input[len - 1];
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[target_feature(enable = "neon")]
|
||||
pub unsafe fn upsample_vertical_neon(
|
||||
input: &[i16], in_near: &[i16], in_far: &[i16], scratch: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert_eq!(in_near.len(), input.len());
|
||||
assert_eq!(in_far.len(), input.len());
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 16 {
|
||||
return super::scalar::upsample_vertical(input, in_near, in_far, scratch, output);
|
||||
}
|
||||
|
||||
let middle = output.len() / 2;
|
||||
let (out_top, out_bottom) = output.split_at_mut(middle);
|
||||
|
||||
// SAFETY: NEON target feature is enabled on this function.
|
||||
let v_three = unsafe { vdupq_n_s16(3) };
|
||||
// SAFETY: NEON target feature is enabled on this function.
|
||||
let v_two = unsafe { vdupq_n_s16(2) };
|
||||
|
||||
let upsample8 = |input: &[i16; 8],
|
||||
in_near: &[i16; 8],
|
||||
in_far: &[i16; 8],
|
||||
out_top: &mut [i16; 8],
|
||||
out_bottom: &mut [i16; 8]| {
|
||||
// SAFETY: Inputs are all 8 * 16 bit long, so the loads are safe.
|
||||
let (v_in, v_near, v_far) = unsafe {
|
||||
(
|
||||
vld1q_s16(input.as_ptr()),
|
||||
vld1q_s16(in_near.as_ptr()),
|
||||
vld1q_s16(in_far.as_ptr())
|
||||
)
|
||||
};
|
||||
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_common = unsafe { vaddq_s16(vmulq_s16(v_in, v_three), v_two) };
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_out_top = unsafe { vshrq_n_s16::<2>(vaddq_s16(v_common, v_near)) };
|
||||
// SAFETY: NEON target feature is enabled and vector lanes are valid.
|
||||
let v_out_bottom = unsafe { vshrq_n_s16::<2>(vaddq_s16(v_common, v_far)) };
|
||||
|
||||
// SAFETY: Outputs are 8 * 16 bit long, so the stores are safe.
|
||||
unsafe {
|
||||
vst1q_s16(out_top.as_mut_ptr(), v_out_top);
|
||||
vst1q_s16(out_bottom.as_mut_ptr(), v_out_bottom);
|
||||
}
|
||||
};
|
||||
|
||||
let chunks = input
|
||||
.chunks_exact(8)
|
||||
.zip(in_near.chunks_exact(8))
|
||||
.zip(in_far.chunks_exact(8))
|
||||
.zip(out_top.chunks_exact_mut(8))
|
||||
.zip(out_bottom.chunks_exact_mut(8));
|
||||
|
||||
for ((((input, in_near), in_far), out_top), out_bottom) in chunks {
|
||||
upsample8(
|
||||
input.try_into().unwrap(),
|
||||
in_near.try_into().unwrap(),
|
||||
in_far.try_into().unwrap(),
|
||||
out_top.try_into().unwrap(),
|
||||
out_bottom.try_into().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Upsample the remainder.
|
||||
if let Some(rest) = input.last_chunk::<8>() {
|
||||
if let Some(rest_near) = in_near.last_chunk::<8>() {
|
||||
if let Some(rest_far) = in_far.last_chunk::<8>() {
|
||||
if let Some(mut rest_top) = out_top.last_chunk_mut::<8>() {
|
||||
if let Some(mut rest_bottom) = out_bottom.last_chunk_mut::<8>() {
|
||||
upsample8(rest, rest_near, rest_far, &mut rest_top, &mut rest_bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[target_feature(enable = "neon")]
|
||||
pub unsafe fn upsample_hv_neon(
|
||||
input: &[i16], in_near: &[i16], in_far: &[i16], scratch_space: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
assert_eq!(input.len() * 4, output.len());
|
||||
|
||||
assert!(input.len() * 2 <= scratch_space.len());
|
||||
let scratch_space = &mut scratch_space[..input.len() * 2];
|
||||
|
||||
unsafe { upsample_vertical_neon(input, in_near, in_far, &mut [], scratch_space) };
|
||||
|
||||
let scratch_half = scratch_space.len() / 2;
|
||||
let output_half = output.len() / 2;
|
||||
|
||||
let (scratch_top, scratch_bottom) = scratch_space.split_at_mut(scratch_half);
|
||||
let (out_top, out_bottom) = output.split_at_mut(output_half);
|
||||
|
||||
let mut t = [0];
|
||||
unsafe { upsample_horizontal_neon(scratch_top, &[], &[], &mut t, out_top) };
|
||||
unsafe { upsample_horizontal_neon(scratch_bottom, &[], &[], &mut t, out_bottom) };
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
use std::simd::prelude::*;
|
||||
|
||||
const LANES: usize = 16;
|
||||
type V = Simd<i16, LANES>;
|
||||
|
||||
pub fn upsample_horizontal_simd(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert!(input.len() > 2);
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 18 {
|
||||
return super::scalar::upsample_horizontal(input, in_near, in_far, scratch, output);
|
||||
}
|
||||
|
||||
// First two pixels
|
||||
output[0] = input[0];
|
||||
output[1] = (input[0] * 3 + input[1] + 2) >> 2;
|
||||
|
||||
let v_three = V::splat(3);
|
||||
let v_two = V::splat(2);
|
||||
|
||||
let upsample16 = |input: &[i16; 18], output: &mut [i16; 32]| {
|
||||
let v_prev = V::from_slice(&input[0..LANES]);
|
||||
let v_curr = V::from_slice(&input[1..LANES + 1]);
|
||||
let v_next = V::from_slice(&input[2..LANES + 2]);
|
||||
|
||||
let v_common = v_curr * v_three + v_two;
|
||||
|
||||
let v_even = (v_common + v_prev) >> 2;
|
||||
let v_odd = (v_common + v_next) >> 2;
|
||||
|
||||
let (v_res_1, v_res_2) = v_even.interleave(v_odd);
|
||||
|
||||
v_res_1.copy_to_slice(&mut output[0..LANES]);
|
||||
v_res_2.copy_to_slice(&mut output[LANES..2 * LANES]);
|
||||
};
|
||||
|
||||
for (input, output) in input
|
||||
.windows(18)
|
||||
.step_by(16)
|
||||
.zip(output[2..].chunks_exact_mut(32))
|
||||
{
|
||||
upsample16(input.try_into().unwrap(), output.try_into().unwrap());
|
||||
}
|
||||
|
||||
// Upsample the remainder. This may have some overlap, but that's fine.
|
||||
if let Some(rest_input) = input.last_chunk::<18>() {
|
||||
let end = output.len() - 2;
|
||||
if let Some(rest_output) = output[..end].last_chunk_mut::<32>() {
|
||||
upsample16(rest_input, rest_output);
|
||||
}
|
||||
}
|
||||
|
||||
// Last two pixels.
|
||||
output[output.len() - 2] = (3 * input[len - 1] + input[len - 2] + 2) >> 2;
|
||||
output[output.len() - 1] = input[len - 1];
|
||||
}
|
||||
|
||||
pub fn upsample_vertical_simd(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
_scratch_space: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert_eq!(in_near.len(), input.len());
|
||||
assert_eq!(in_far.len(), input.len());
|
||||
|
||||
let len = input.len();
|
||||
|
||||
if len < 16 {
|
||||
return super::scalar::upsample_vertical(input, in_near, in_far, _scratch_space, output);
|
||||
}
|
||||
|
||||
let middle = output.len() / 2;
|
||||
let (out_top, out_bottom) = output.split_at_mut(middle);
|
||||
|
||||
let v_three = V::splat(3);
|
||||
let v_two = V::splat(2);
|
||||
|
||||
let upsample16 = |input: &[i16; 16],
|
||||
in_near: &[i16; 16],
|
||||
in_far: &[i16; 16],
|
||||
out_top: &mut [i16; 16],
|
||||
out_bottom: &mut [i16; 16]| {
|
||||
let v_in = V::from(*input);
|
||||
let v_near = V::from(*in_near);
|
||||
let v_far = V::from(*in_far);
|
||||
|
||||
let v_common = v_in * v_three + v_two;
|
||||
|
||||
let v_out_top = (v_common + v_near) >> 2;
|
||||
let v_out_bottom = (v_common + v_far) >> 2;
|
||||
|
||||
v_out_top.copy_to_slice(out_top.as_mut_slice());
|
||||
v_out_bottom.copy_to_slice(out_bottom.as_mut_slice());
|
||||
};
|
||||
|
||||
let chunks = input
|
||||
.chunks_exact(16)
|
||||
.zip(in_near.chunks_exact(16))
|
||||
.zip(in_far.chunks_exact(16))
|
||||
.zip(out_top.chunks_exact_mut(16))
|
||||
.zip(out_bottom.chunks_exact_mut(16));
|
||||
|
||||
for ((((input, in_near), in_far), out_top), out_bottom) in chunks {
|
||||
upsample16(
|
||||
input.try_into().unwrap(),
|
||||
in_near.try_into().unwrap(),
|
||||
in_far.try_into().unwrap(),
|
||||
out_top.try_into().unwrap(),
|
||||
out_bottom.try_into().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Upsample the remainder. This may have some overlap, but that's fine.
|
||||
// Edition upgrade will fix this nested awfulness.
|
||||
if let Some(rest) = input.last_chunk::<16>() {
|
||||
if let Some( rest_near) = in_near.last_chunk::<16>() {
|
||||
if let Some( rest_far) = in_far.last_chunk::<16>() {
|
||||
if let Some( rest_top) = out_top.last_chunk_mut::<16>() {
|
||||
if let Some( rest_bottom) = out_bottom.last_chunk_mut::<16>() {
|
||||
upsample16(rest, rest_near, rest_far, rest_top, rest_bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upsample_hv_simd(
|
||||
input: &[i16],
|
||||
in_near: &[i16],
|
||||
in_far: &[i16],
|
||||
scratch_space: &mut [i16],
|
||||
output: &mut [i16],
|
||||
) {
|
||||
assert_eq!(input.len() * 4, output.len());
|
||||
|
||||
assert!(input.len() * 2 <= scratch_space.len());
|
||||
let scratch_space = &mut scratch_space[..input.len() * 2];
|
||||
|
||||
|
||||
upsample_vertical_simd(input, in_near, in_far, &mut [], scratch_space);
|
||||
|
||||
let scratch_half = scratch_space.len() / 2;
|
||||
let output_half = output.len() / 2;
|
||||
|
||||
let (scratch_top, scratch_bottom) = scratch_space.split_at_mut(scratch_half);
|
||||
let (out_top, out_bottom) = output.split_at_mut(output_half);
|
||||
|
||||
let mut t = [0];
|
||||
upsample_horizontal_simd(scratch_top, &[], &[], &mut t, out_top);
|
||||
upsample_horizontal_simd(scratch_bottom, &[], &[], &mut t, out_bottom);
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
pub fn upsample_horizontal(
|
||||
input: &[i16], _ref: &[i16], _in_near: &[i16], _scratch: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
assert_eq!(
|
||||
input.len() * 2,
|
||||
output.len(),
|
||||
"Input length is not half the size of the output length"
|
||||
);
|
||||
assert!(
|
||||
output.len() > 4 && input.len() > 2,
|
||||
"Too Short of a vector, cannot upsample"
|
||||
);
|
||||
|
||||
output[0] = input[0];
|
||||
output[1] = (input[0] * 3 + input[1] + 2) >> 2;
|
||||
|
||||
// This code is written for speed and not readability
|
||||
//
|
||||
// The readable code is
|
||||
//
|
||||
// for i in 1..input.len() - 1{
|
||||
// let sample = 3 * input[i] + 2;
|
||||
// out[i * 2] = (sample + input[i - 1]) >> 2;
|
||||
// out[i * 2 + 1] = (sample + input[i + 1]) >> 2;
|
||||
// }
|
||||
//
|
||||
// The output of a pixel is determined by it's surrounding neighbours but we attach more weight to it's nearest
|
||||
// neighbour (input[i]) than to the next nearest neighbour.
|
||||
|
||||
for (output_window, input_window) in output[2..].chunks_exact_mut(2).zip(input.windows(3)) {
|
||||
let sample = 3 * input_window[1] + 2;
|
||||
|
||||
output_window[0] = (sample + input_window[0]) >> 2;
|
||||
output_window[1] = (sample + input_window[2]) >> 2;
|
||||
}
|
||||
// Get lengths
|
||||
let out_len = output.len() - 2;
|
||||
let input_len = input.len() - 2;
|
||||
|
||||
// slice the output vector
|
||||
let f_out = &mut output[out_len..];
|
||||
let i_last = &input[input_len..];
|
||||
|
||||
// write out manually..
|
||||
f_out[0] = (3 * i_last[1] + i_last[0] + 2) >> 2;
|
||||
f_out[1] = i_last[1];
|
||||
}
|
||||
pub fn upsample_vertical(
|
||||
input: &[i16], in_near: &[i16], in_far: &[i16], _scratch_space: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
assert_eq!(input.len() * 2, output.len());
|
||||
assert_eq!(in_near.len(), input.len());
|
||||
assert_eq!(in_far.len(), input.len());
|
||||
|
||||
let middle = output.len() / 2;
|
||||
|
||||
let (out_top, out_bottom) = output.split_at_mut(middle);
|
||||
|
||||
// for the first row, closest row is in_near
|
||||
for ((near, far), x) in input.iter().zip(in_near.iter()).zip(out_top) {
|
||||
*x = (((3 * near) + 2) + far) >> 2;
|
||||
}
|
||||
// for the second row, the closest row to input is in_far
|
||||
for ((near, far), x) in input.iter().zip(in_far.iter()).zip(out_bottom) {
|
||||
*x = (((3 * near) + 2) + far) >> 2;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upsample_hv(
|
||||
input: &[i16], in_near: &[i16], in_far: &[i16], scratch_space: &mut [i16], output: &mut [i16]
|
||||
) {
|
||||
|
||||
assert_eq!(input.len() * 4, output.len());
|
||||
|
||||
assert!(input.len() * 2 <= scratch_space.len());
|
||||
let scratch_space = &mut scratch_space[..input.len() * 2];
|
||||
|
||||
|
||||
let mut t = [0];
|
||||
upsample_vertical(input, in_near, in_far, &mut t, scratch_space);
|
||||
// horizontal upsampling must be done separate for every line
|
||||
// Otherwise it introduces artifacts that may cause the edge colors
|
||||
// to appear on the other line.
|
||||
|
||||
// Since this is called for two scanlines/widths currently
|
||||
// splitting the inputs and outputs into half ensures we only handle
|
||||
// one scanline per iteration
|
||||
let scratch_half = scratch_space.len() / 2;
|
||||
|
||||
let output_half = output.len() / 2;
|
||||
|
||||
upsample_horizontal(
|
||||
&scratch_space[..scratch_half],
|
||||
&[],
|
||||
&[],
|
||||
&mut t,
|
||||
&mut output[..output_half]
|
||||
);
|
||||
|
||||
upsample_horizontal(
|
||||
&scratch_space[scratch_half..],
|
||||
&[],
|
||||
&[],
|
||||
&mut t,
|
||||
&mut output[output_half..]
|
||||
);
|
||||
}
|
||||
|
||||
pub fn upsample_generic(
|
||||
input: &[i16], _in_near: &[i16], _in_far: &[i16], _scratch_space: &mut [i16],
|
||||
output: &mut [i16]
|
||||
) {
|
||||
// use nearest sample
|
||||
let difference = output.len() / input.len();
|
||||
if difference > 0 {
|
||||
// nearest neighbour
|
||||
for (input, chunk_output) in input.iter().zip(output.chunks_exact_mut(difference)) {
|
||||
chunk_output.iter_mut().for_each(|x| *x = *input);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+577
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
* Copyright (c) 2023.
|
||||
*
|
||||
* This software is free software;
|
||||
*
|
||||
* You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
|
||||
*/
|
||||
|
||||
use alloc::format;
|
||||
use core::convert::TryInto;
|
||||
use core::cmp::min;
|
||||
|
||||
use zune_core::colorspace::ColorSpace;
|
||||
|
||||
use crate::color_convert::ycbcr_to_grayscale;
|
||||
use crate::components::{Components, SampleRatios};
|
||||
use crate::decoder::{ColorConvert16Ptr, MAX_COMPONENTS};
|
||||
use crate::errors::DecodeErrors;
|
||||
|
||||
/// fast 0..255 * 0..255 => 0..255 rounded multiplication
|
||||
///
|
||||
/// Borrowed from stb
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
#[inline]
|
||||
fn blinn_8x8(in_val: u8, y: u8) -> u8 {
|
||||
let t = i32::from(in_val) * i32::from(y) + 128;
|
||||
return ((t + (t >> 8)) >> 8) as u8;
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
pub(crate) fn color_convert(
|
||||
unprocessed: &[&[i16]; MAX_COMPONENTS], color_convert_16: ColorConvert16Ptr,
|
||||
input_colorspace: ColorSpace, output_colorspace: ColorSpace, output: &mut [u8], width: usize,
|
||||
padded_width: usize
|
||||
) -> Result<(), DecodeErrors> {
|
||||
if input_colorspace.num_components() == 3 && input_colorspace == output_colorspace {
|
||||
// sort things like RGB to RGB conversion
|
||||
copy_removing_padding(unprocessed, width, padded_width, output);
|
||||
return Ok(());
|
||||
}
|
||||
if input_colorspace.num_components() == 4 && input_colorspace == output_colorspace {
|
||||
copy_removing_padding_4x(unprocessed, width, padded_width, output);
|
||||
return Ok(());
|
||||
}
|
||||
// color convert
|
||||
match (input_colorspace, output_colorspace) {
|
||||
(ColorSpace::YCbCr | ColorSpace::Luma, ColorSpace::Luma) => {
|
||||
ycbcr_to_grayscale(unprocessed[0], width, padded_width, output);
|
||||
}
|
||||
(
|
||||
ColorSpace::YCbCr,
|
||||
ColorSpace::RGB | ColorSpace::RGBA | ColorSpace::BGR | ColorSpace::BGRA
|
||||
) => {
|
||||
color_convert_ycbcr(
|
||||
unprocessed,
|
||||
width,
|
||||
padded_width,
|
||||
output_colorspace,
|
||||
color_convert_16,
|
||||
output
|
||||
);
|
||||
}
|
||||
(ColorSpace::YCCK, ColorSpace::RGB) => {
|
||||
color_convert_ycck_to_rgb::<3>(
|
||||
unprocessed,
|
||||
width,
|
||||
padded_width,
|
||||
output_colorspace,
|
||||
color_convert_16,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
(ColorSpace::YCCK, ColorSpace::RGBA) => {
|
||||
color_convert_ycck_to_rgb::<4>(
|
||||
unprocessed,
|
||||
width,
|
||||
padded_width,
|
||||
output_colorspace,
|
||||
color_convert_16,
|
||||
output
|
||||
);
|
||||
}
|
||||
(ColorSpace::CMYK, ColorSpace::RGB) => {
|
||||
color_convert_cymk_to_rgb::<3>(unprocessed, width, padded_width, output);
|
||||
}
|
||||
(ColorSpace::CMYK, ColorSpace::RGBA) => {
|
||||
color_convert_cymk_to_rgb::<4>(unprocessed, width, padded_width, output);
|
||||
}
|
||||
(ColorSpace::MultiBand(n), _) => {
|
||||
if n.get() != 2 {
|
||||
return Err(DecodeErrors::Format(format!(
|
||||
"Unknown multiband sample ({n}), please share sample"
|
||||
)));
|
||||
}
|
||||
copy_removing_padding_generic(
|
||||
unprocessed,
|
||||
width,
|
||||
padded_width,
|
||||
output,
|
||||
n.get() as usize
|
||||
);
|
||||
}
|
||||
(ColorSpace::Luma, ColorSpace::RGB) => {
|
||||
// duplicate the luma channel three times to form RGB
|
||||
// Note, this may assume the direct conversion
|
||||
// from luma to RGB is by duplicating
|
||||
//
|
||||
// There may be a bit more complex ways
|
||||
// of doing it but won't get onto it
|
||||
convert_luma_to_rgb(unprocessed, width, padded_width, output)
|
||||
}
|
||||
(ColorSpace::Luma, ColorSpace::RGBA) => {
|
||||
// duplicate the luma channel three times to form RGB
|
||||
// add 255 as alpha
|
||||
// Note, this may assume the direct conversion
|
||||
// from luma to RGB is by duplicating
|
||||
//
|
||||
// There may be a bit more complex ways
|
||||
// of doing it but won't get onto it
|
||||
convert_luma_to_rgba(unprocessed, width, padded_width, output)
|
||||
}
|
||||
|
||||
// For the other components we do nothing(currently)
|
||||
_ => {
|
||||
let msg = format!(
|
||||
"Unimplemented colorspace mapping from {input_colorspace:?} to {output_colorspace:?}");
|
||||
|
||||
return Err(DecodeErrors::Format(msg));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_luma_to_rgb(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8]
|
||||
) {
|
||||
for (pix_w, y_w) in output
|
||||
.chunks_exact_mut(width * 3)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
{
|
||||
for (pix, c) in pix_w.chunks_exact_mut(3).zip(y_w) {
|
||||
pix[0] = *c as u8;
|
||||
pix[1] = *c as u8;
|
||||
pix[2] = *c as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn convert_luma_to_rgba(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8]
|
||||
) {
|
||||
for (pix_w, y_w) in output
|
||||
.chunks_exact_mut(width * 4)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
{
|
||||
for (pix, c) in pix_w.chunks_exact_mut(4).zip(y_w) {
|
||||
pix[0] = *c as u8;
|
||||
pix[1] = *c as u8;
|
||||
pix[2] = *c as u8;
|
||||
pix[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Copy a block to output removing padding bytes from input
|
||||
/// if necessary
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
fn copy_removing_padding(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8]
|
||||
) {
|
||||
for (((pix_w, c_w), m_w), y_w) in output
|
||||
.chunks_exact_mut(width * 3)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
.zip(mcu_block[1].chunks_exact(padded_width))
|
||||
.zip(mcu_block[2].chunks_exact(padded_width))
|
||||
{
|
||||
for (((pix, c), y), m) in pix_w.chunks_exact_mut(3).zip(c_w).zip(m_w).zip(y_w) {
|
||||
pix[0] = *c as u8;
|
||||
pix[1] = *y as u8;
|
||||
pix[2] = *m as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
fn copy_removing_padding_4x(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8]
|
||||
) {
|
||||
for ((((pix_w, c_w), m_w), y_w), k_w) in output
|
||||
.chunks_exact_mut(width * 4)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
.zip(mcu_block[1].chunks_exact(padded_width))
|
||||
.zip(mcu_block[2].chunks_exact(padded_width))
|
||||
.zip(mcu_block[3].chunks_exact(padded_width))
|
||||
{
|
||||
for ((((pix, c), y), m), k) in pix_w
|
||||
.chunks_exact_mut(4)
|
||||
.zip(c_w)
|
||||
.zip(m_w)
|
||||
.zip(y_w)
|
||||
.zip(k_w)
|
||||
{
|
||||
pix[0] = *c as u8;
|
||||
pix[1] = *y as u8;
|
||||
pix[2] = *m as u8;
|
||||
pix[3] = *k as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
fn copy_removing_padding_generic(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8],
|
||||
channels: usize
|
||||
) {
|
||||
match channels {
|
||||
// just do 2 for now
|
||||
2 => {
|
||||
for ((pix_w, y_w), k_w) in output
|
||||
.chunks_exact_mut(width * channels)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
.zip(mcu_block[1].chunks_exact(padded_width))
|
||||
{
|
||||
for ((pix, c), k) in pix_w.chunks_exact_mut(2).zip(y_w).zip(k_w) {
|
||||
pix[0] = *c as u8;
|
||||
pix[1] = *k as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
/// Convert YCCK image to rgb
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
fn color_convert_ycck_to_rgb<const NUM_COMPONENTS: usize>(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize,
|
||||
output_colorspace: ColorSpace, color_convert_16: ColorConvert16Ptr, output: &mut [u8]
|
||||
) {
|
||||
color_convert_ycbcr(
|
||||
mcu_block,
|
||||
width,
|
||||
padded_width,
|
||||
output_colorspace,
|
||||
color_convert_16,
|
||||
output
|
||||
);
|
||||
for (pix_w, m_w) in output
|
||||
.chunks_exact_mut(width * 3)
|
||||
.zip(mcu_block[3].chunks_exact(padded_width))
|
||||
{
|
||||
for (pix, m) in pix_w.chunks_exact_mut(NUM_COMPONENTS).zip(m_w) {
|
||||
let m = (*m) as u8;
|
||||
pix[0] = blinn_8x8(255 - pix[0], m);
|
||||
pix[1] = blinn_8x8(255 - pix[1], m);
|
||||
pix[2] = blinn_8x8(255 - pix[2], m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
fn color_convert_cymk_to_rgb<const NUM_COMPONENTS: usize>(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize, output: &mut [u8]
|
||||
) {
|
||||
for ((((pix_w, c_w), m_w), y_w), k_w) in output
|
||||
.chunks_exact_mut(width * NUM_COMPONENTS)
|
||||
.zip(mcu_block[0].chunks_exact(padded_width))
|
||||
.zip(mcu_block[1].chunks_exact(padded_width))
|
||||
.zip(mcu_block[2].chunks_exact(padded_width))
|
||||
.zip(mcu_block[3].chunks_exact(padded_width))
|
||||
{
|
||||
for ((((pix, c), m), y), k) in pix_w
|
||||
.chunks_exact_mut(3)
|
||||
.zip(c_w)
|
||||
.zip(m_w)
|
||||
.zip(y_w)
|
||||
.zip(k_w)
|
||||
{
|
||||
let c = *c as u8;
|
||||
let m = *m as u8;
|
||||
let y = *y as u8;
|
||||
let k = *k as u8;
|
||||
|
||||
pix[0] = blinn_8x8(c, k);
|
||||
pix[1] = blinn_8x8(m, k);
|
||||
pix[2] = blinn_8x8(y, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Do color-conversion for interleaved MCU
|
||||
#[allow(
|
||||
clippy::similar_names,
|
||||
clippy::too_many_arguments,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::unwrap_used
|
||||
)]
|
||||
fn color_convert_ycbcr(
|
||||
mcu_block: &[&[i16]; MAX_COMPONENTS], width: usize, padded_width: usize,
|
||||
output_colorspace: ColorSpace, color_convert_16: ColorConvert16Ptr, output: &mut [u8]
|
||||
) {
|
||||
let num_components = output_colorspace.num_components();
|
||||
|
||||
let stride = width * num_components;
|
||||
// Allocate temporary buffer for small widths less than 16.
|
||||
let mut temp = [0; 64];
|
||||
// We need to chunk per width to ensure we can discard extra values at the end of the width.
|
||||
// Since the encoder may pad bits to ensure the width is a multiple of 8.
|
||||
for (((y_width, cb_width), cr_width), out) in mcu_block[0]
|
||||
.chunks_exact(padded_width)
|
||||
.zip(mcu_block[1].chunks_exact(padded_width))
|
||||
.zip(mcu_block[2].chunks_exact(padded_width))
|
||||
.zip(output.chunks_exact_mut(stride))
|
||||
{
|
||||
if width < 16 {
|
||||
// allocate temporary buffers for the values received from idct
|
||||
let mut y_out = [0; 16];
|
||||
let mut cb_out = [0; 16];
|
||||
let mut cr_out = [0; 16];
|
||||
// copy those small widths to that buffer
|
||||
// Use a min with 16 to prevent some panics, see https://github.com/etemesi254/zune-image/issues/331
|
||||
y_out[0..min(y_width.len(), 16)].copy_from_slice(&y_width[0..min(y_width.len(), 16)]);
|
||||
cb_out[0..min(cb_width.len(), 16)]
|
||||
.copy_from_slice(&cb_width[0..min(cb_width.len(), 16)]);
|
||||
cr_out[0..min(cr_width.len(), 16)]
|
||||
.copy_from_slice(&cr_width[0..min(cr_width.len(), 16)]);
|
||||
// we handle widths less than 16 a bit differently, allocating a temporary
|
||||
// buffer and writing to that and then flushing to the out buffer
|
||||
// because of the optimizations applied below,
|
||||
(color_convert_16)(&y_out, &cb_out, &cr_out, &mut temp, &mut 0);
|
||||
// copy to stride
|
||||
out[0..width * num_components].copy_from_slice(&temp[0..width * num_components]);
|
||||
// next
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chunk in outputs of 16 to pass to color_convert as an array of 16 i16's.
|
||||
for (((y, cb), cr), out_c) in y_width
|
||||
.chunks_exact(16)
|
||||
.zip(cb_width.chunks_exact(16))
|
||||
.zip(cr_width.chunks_exact(16))
|
||||
.zip(out.chunks_exact_mut(16 * num_components))
|
||||
{
|
||||
(color_convert_16)(
|
||||
y.try_into().unwrap(),
|
||||
cb.try_into().unwrap(),
|
||||
cr.try_into().unwrap(),
|
||||
out_c,
|
||||
&mut 0
|
||||
);
|
||||
}
|
||||
//we have more pixels in the end that can't be handled by the main loop.
|
||||
//move pointer back a little bit to get last 16 bytes,
|
||||
//color convert, and overwrite
|
||||
//This means some values will be color converted twice.
|
||||
for ((y, cb), cr) in y_width[width - 16..]
|
||||
.chunks_exact(16)
|
||||
.zip(cb_width[width - 16..].chunks_exact(16))
|
||||
.zip(cr_width[width - 16..].chunks_exact(16))
|
||||
.take(1)
|
||||
{
|
||||
(color_convert_16)(
|
||||
y.try_into().unwrap(),
|
||||
cb.try_into().unwrap(),
|
||||
cr.try_into().unwrap(),
|
||||
&mut temp,
|
||||
&mut 0
|
||||
);
|
||||
}
|
||||
|
||||
let rem = out[(width - 16) * num_components..]
|
||||
.chunks_exact_mut(16 * num_components)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
rem.copy_from_slice(&temp[0..rem.len()]);
|
||||
}
|
||||
}
|
||||
pub(crate) fn upsample(
|
||||
component: &mut Components, mcu_height: usize, i: usize, upsampler_scratch_space: &mut [i16],
|
||||
has_vertical_sample: bool
|
||||
) -> Result<(), DecodeErrors> {
|
||||
match component.sample_ratio {
|
||||
SampleRatios::V | SampleRatios::HV => {
|
||||
/*
|
||||
When upsampling vertically sampled images, we have a certain problem
|
||||
which is that we do not have all MCU's decoded, this usually sucks at boundaries
|
||||
e.g we can't upsample the last mcu row, since the row_down currently doesn't exist
|
||||
|
||||
To solve this we need to do two things
|
||||
|
||||
1. Carry over coefficients when we lack enough data to upsample
|
||||
2. Upsample when we have enough data
|
||||
|
||||
To achieve (1), we store a previous row, and the current row in components themselves
|
||||
which will later be used to make (2)
|
||||
|
||||
To achieve (2), we take the stored previous row(second last MCU row),
|
||||
current row(last mcu row) and row down(first row of newly decoded MCU)
|
||||
|
||||
and upsample that and store it in first_row_upsample_dest, this contains
|
||||
up-sampled coefficients for the last for the previous decoded mcu row.
|
||||
|
||||
The caller is then expected to process first_row_upsample_dest before processing data
|
||||
in component.upsample_dest which stores the up-sampled components excluding the last row
|
||||
*/
|
||||
|
||||
let mut dest_start = 0;
|
||||
let stride_bytes_written = component.width_stride * component.sample_ratio.sample();
|
||||
|
||||
if i > 0 {
|
||||
// Handle the last MCU of the previous row
|
||||
// This wasn't up-sampled as we didn't have the row_down
|
||||
// so we do it now
|
||||
|
||||
let stride = component.width_stride;
|
||||
|
||||
let dest = &mut component.first_row_upsample_dest[0..stride_bytes_written];
|
||||
|
||||
// get current row
|
||||
let row = &component.row[..];
|
||||
let row_up = &component.row_up[..];
|
||||
let row_down = &component.raw_coeff[0..stride];
|
||||
(component.up_sampler)(row, row_up, row_down, upsampler_scratch_space, dest);
|
||||
}
|
||||
|
||||
// we have the Y component width stride.
|
||||
// this may be higher than the actual width,(2x because vertical sampling)
|
||||
//
|
||||
// This will not upsample the last row
|
||||
|
||||
// if false, do not upsample.
|
||||
// set to false on the last row of an mcu
|
||||
let mut upsample = true;
|
||||
|
||||
let stride = component.width_stride * component.vertical_sample;
|
||||
let stop_offset = component.raw_coeff.len() / component.width_stride;
|
||||
|
||||
if component.raw_coeff.len() != stop_offset * stride {
|
||||
// slice would panic below
|
||||
return Err(DecodeErrors::FormatStatic(
|
||||
"Invalid component dimensions, would panic"
|
||||
));
|
||||
}
|
||||
for (pos, curr_row) in component
|
||||
.raw_coeff
|
||||
.chunks_exact(component.width_stride)
|
||||
.enumerate()
|
||||
{
|
||||
let mut dest: &mut [i16] = &mut [];
|
||||
let mut row_up: &[i16] = &[];
|
||||
// row below current sample
|
||||
let mut row_down: &[i16] = &[];
|
||||
|
||||
// Order of ifs matters
|
||||
|
||||
if i == 0 && pos == 0 {
|
||||
// first IMAGE row, row_up is the same as current row
|
||||
// row_down is the row below.
|
||||
row_up = &component.raw_coeff[pos * stride..(pos + 1) * stride];
|
||||
row_down = &component.raw_coeff[(pos + 1) * stride..(pos + 2) * stride];
|
||||
} else if i > 0 && pos == 0 {
|
||||
// first row of a new mcu, previous row was copied so use that
|
||||
row_up = &component.row[..];
|
||||
row_down = &component.raw_coeff[(pos + 1) * stride..(pos + 2) * stride];
|
||||
} else if i == mcu_height.saturating_sub(1) && pos == stop_offset - 1 {
|
||||
// last IMAGE row, adjust pointer to use previous row and current row
|
||||
row_up = &component.raw_coeff[(pos - 1) * stride..pos * stride];
|
||||
row_down = &component.raw_coeff[pos * stride..(pos + 1) * stride];
|
||||
} else if pos > 0 && pos < stop_offset - 1 {
|
||||
// other rows, get row up and row down relative to our current row
|
||||
// ignore last row of each mcu
|
||||
row_up = &component.raw_coeff[(pos - 1) * stride..pos * stride];
|
||||
row_down = &component.raw_coeff[(pos + 1) * stride..(pos + 2) * stride];
|
||||
} else if pos == stop_offset - 1 {
|
||||
// last MCU in a row
|
||||
//
|
||||
// we need a row at the next MCU but we haven't decoded that MCU yet
|
||||
// so we should save this and when we have the next MCU,
|
||||
// do the upsampling
|
||||
|
||||
// store the current row and previous row in a buffer
|
||||
let prev_row = &component.raw_coeff[(pos - 1) * stride..pos * stride];
|
||||
|
||||
component.row_up.copy_from_slice(prev_row);
|
||||
component.row.copy_from_slice(curr_row);
|
||||
upsample = false;
|
||||
} else {
|
||||
unreachable!("Uh oh!");
|
||||
}
|
||||
if upsample {
|
||||
dest =
|
||||
&mut component.upsample_dest[dest_start..dest_start + stride_bytes_written];
|
||||
dest_start += stride_bytes_written;
|
||||
}
|
||||
|
||||
if upsample {
|
||||
// upsample
|
||||
(component.up_sampler)(
|
||||
curr_row,
|
||||
row_up,
|
||||
row_down,
|
||||
upsampler_scratch_space,
|
||||
dest
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SampleRatios::H => {
|
||||
//assert_eq!(component.raw_coeff.len() * 2, component.upsample_dest.len());
|
||||
// Before it was an assert, but numerous and numerous and numerous
|
||||
// bug fixes and ad hoc solutions later, I have now just decided to keep it as a resize
|
||||
component
|
||||
.upsample_dest
|
||||
.resize(component.raw_coeff.len() * 2, 0);
|
||||
|
||||
let raw_coeff = &component.raw_coeff;
|
||||
let dest_coeff = &mut component.upsample_dest;
|
||||
|
||||
if has_vertical_sample {
|
||||
/*
|
||||
There have been images that have the following configurations.
|
||||
|
||||
Component ID:Y HS:2 VS:2 QT:0
|
||||
Component ID:Cb HS:1 VS:1 QT:1
|
||||
Component ID:Cr HS:1 VS:2 QT:1
|
||||
|
||||
This brings out a nasty case of misaligned sampling factors. Cr will need to save a row because
|
||||
of the way we process boundaries but Cb won't since Cr is horizontally sampled while Cb is
|
||||
HV sampled with respect to the image sampling factors.
|
||||
|
||||
So during decoding of one MCU, we could only do 7 and not 8 rows, but the SampleRatio::H never had to
|
||||
save a single line, since it doesn't suffer from boundary issues.
|
||||
|
||||
Now this takes care of that, saving the last MCU row in case it will be needed.
|
||||
We save the previous row before up-sampling this row because the boundary issue is in
|
||||
the last MCU row of the previous MCU.
|
||||
|
||||
PS(cae): I can't add the image to the repo as it is nsfw, but can send if required
|
||||
*/
|
||||
let length = component.first_row_upsample_dest.len();
|
||||
component
|
||||
.first_row_upsample_dest
|
||||
.copy_from_slice(&dest_coeff.rchunks_exact(length).next().unwrap());
|
||||
}
|
||||
// up-sample each row
|
||||
for (single_row, output_stride) in raw_coeff
|
||||
.chunks_exact(component.width_stride)
|
||||
.zip(dest_coeff.chunks_exact_mut(component.width_stride * 2))
|
||||
{
|
||||
// upsample using the fn pointer, should only be H, so no need for
|
||||
// row up and row down
|
||||
(component.up_sampler)(single_row, &[], &[], &mut [], output_stride);
|
||||
}
|
||||
}
|
||||
SampleRatios::Generic(h, v) => {
|
||||
let raw_coeff = &component.raw_coeff;
|
||||
let dest_coeff = &mut component.upsample_dest;
|
||||
|
||||
//let size = component.width_stride.div_ceil(v);
|
||||
|
||||
// for (single_row, output_stride) in raw_coeff
|
||||
// .chunks_exact(size)
|
||||
// .zip(dest_coeff.chunks_exact_mut(component.width_stride * h))
|
||||
// {
|
||||
// (component.up_sampler)(single_row, &[], &[], &mut [], output_stride);
|
||||
//
|
||||
// }
|
||||
for (single_row, output_stride) in raw_coeff
|
||||
.chunks_exact(component.width_stride)
|
||||
.zip(dest_coeff.chunks_exact_mut(component.width_stride * h * v))
|
||||
{
|
||||
for row in output_stride.chunks_exact_mut(component.width_stride * h) {
|
||||
(component.up_sampler)(single_row, &[], &[], &mut [], row);
|
||||
}
|
||||
}
|
||||
}
|
||||
SampleRatios::None => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -21,7 +21,7 @@ RUN rustup target add \
|
||||
# Install cargo-zigbuild for easier cross-compilation (especially macOS)
|
||||
RUN curl -sSL https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ -C /opt \
|
||||
&& ln -s /opt/zig-linux-x86_64-0.13.0/zig /usr/local/bin/zig
|
||||
RUN cargo install cargo-zigbuild
|
||||
RUN cargo install cargo-zigbuild --version 0.21.8
|
||||
|
||||
# Configure linkers for cross-compilation
|
||||
RUN mkdir -p /.cargo
|
||||
|
||||
@@ -109,6 +109,27 @@ flowchart TD
|
||||
|
||||
> 执行链路保护优先于信号修饰。
|
||||
|
||||
### 1.6 反调试执行面:与指纹评分面并行
|
||||
|
||||
很多站点并不只依赖指纹评分,还会部署“主动处置型反调试”脚本。
|
||||
|
||||
典型路径:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["页面启动"] --> B["反调试探测"]
|
||||
B --> C{"命中?"}
|
||||
C -->|是| D["close/back/redirect"]
|
||||
C -->|否| E["继续业务逻辑"]
|
||||
```
|
||||
|
||||
该路径与指纹评分面的关系:
|
||||
|
||||
1. 指纹评分决定“挑战/放行/降权”
|
||||
2. 反调试处置决定“页面是否继续可用”
|
||||
|
||||
因此,“页面自关闭”不能直接推断为“指纹被识别”,更常见是反调试链路触发。
|
||||
|
||||
---
|
||||
|
||||
## 二、控制面(分层设计)
|
||||
@@ -157,6 +178,18 @@ flowchart LR
|
||||
- 优先治理高频、可解释的探测路径
|
||||
- 对跨域挑战链路对象设置严格注入边界
|
||||
|
||||
#### 2.4.1 反调试脚本治理(以 disable-devtool 类库为例)
|
||||
|
||||
反调试脚本通常通过固定启动入口触发(例如 `disable-devtool-auto` 标记)。
|
||||
|
||||
可行控制策略:
|
||||
|
||||
1. 仅抑制其自动启动入口,避免触发主动处置
|
||||
2. 不改写通用查询/脚本加载语义,避免影响业务页面
|
||||
3. 将治理范围限制在高置信度触发点,控制副作用面
|
||||
|
||||
这类策略的本质是“执行面隔离”,不是“伪造更多指纹”。
|
||||
|
||||
### 2.5 行为与会话控制
|
||||
|
||||
目标:塑形时间分布,减少上下文漂移。
|
||||
@@ -183,7 +216,7 @@ Turnstile 场景下的关键控制面可抽象为:
|
||||
|
||||
控制面设计通常按以下优先级推进:
|
||||
|
||||
1. 执行完整性(保证链路可运行)
|
||||
1. 执行完整性(保证链路可运行,包含反调试触发面治理)
|
||||
2. 一致性约束集合(消除跨表面矛盾)
|
||||
3. 稀有性控制(避免低频组合叠加)
|
||||
4. 时序分布塑形(降低机械统计特征)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# agent-browser-stealth 新功能发布
|
||||
|
||||
这次我们对 `agent-browser-stealth` 做了一次完整升级:
|
||||
从“标签分组辅助”升级为“可控制、可观测、可编排”的 AI 浏览器控制台。
|
||||
|
||||
## 这次升级解决了什么问题
|
||||
|
||||
过去插件主要解决会话隔离和分组管理。
|
||||
在真实自动化场景中,仍有三个核心缺口:
|
||||
|
||||
1. 缺少侧边栏直接控页能力
|
||||
2. 缺少执行过程可观测性(console/network/DOM)
|
||||
3. 缺少可复用流程(workflow/shortcut/schedule)
|
||||
|
||||
本次发布一次性补齐这三块。
|
||||
|
||||
## 新增能力一:侧边栏浏览控制
|
||||
|
||||
现在你可以在 side panel 直接完成页面控制:
|
||||
|
||||
- `open / back / forward / reload`
|
||||
- 按 CSS 选择器执行 `click / fill / press`
|
||||
- 标签页切换与关闭
|
||||
- 直接运行快捷指令(slash shortcut)
|
||||
|
||||
这让“流程启动”和“人工微调”可以在同一界面完成。
|
||||
|
||||
## 新增能力二:开发者观测能力(Developer Signals)
|
||||
|
||||
我们新增了调试信号面板,避免黑盒执行:
|
||||
|
||||
- 页面 `console` 事件(含 error/warn)
|
||||
- `fetch/xhr` 网络事件
|
||||
- 命令历史
|
||||
- DOM 状态快照(文本预览、交互元素、mutation 摘要)
|
||||
|
||||
当流程失败时,可以快速判断是页面结构变化、网络问题还是动作配置问题。
|
||||
|
||||
## 新增能力三:Workflow 自动化体系
|
||||
|
||||
插件现在支持完整自动化闭环:
|
||||
|
||||
- 录制:`Start -> Stop -> Save`
|
||||
- 回放:运行 workflow
|
||||
- 复用:绑定 slash shortcut
|
||||
- 调度:`daily / weekly / monthly / yearly`
|
||||
|
||||
调度任务会记录 `lastRunAt / nextRunAt`,并把执行结果写入活动流,便于排查和审计。
|
||||
|
||||
## 已完成实测:workflow + abs 接管流程
|
||||
|
||||
我们验证了一个非常实用的流程:
|
||||
|
||||
1. 先用 workflow 进入目标页面
|
||||
2. 在关键节点停顿(checkpoint)
|
||||
3. 让 AI 使用 `abs` 完成上传/填写等细操作
|
||||
4. 停在发布前,等待人工确认
|
||||
|
||||
这个模式已经在小红书发布场景中跑通:
|
||||
进入发布页、上传图片、填写标题正文,且默认不自动点击“发布”。
|
||||
|
||||
## 兼容性与稳定性改进
|
||||
|
||||
这次还修复了几个关键稳定性问题:
|
||||
|
||||
- 修复 side panel `activeTab` 空值崩溃
|
||||
- 增加面板状态归一化兜底,兼容热更新与旧返回结构
|
||||
- 修复侧边栏渲染的转义问题,避免内容显示异常
|
||||
|
||||
## 总结
|
||||
|
||||
`agent-browser-stealth` 现在不只是“分组插件”,而是一个真正可用于生产流的浏览器执行层:
|
||||
|
||||
- 可控:侧边栏直接控页 + 分段执行
|
||||
- 可观测:console/network/DOM 全链路可见
|
||||
- 可复用:workflow + shortcut + schedule
|
||||
|
||||
如果你在做 AI 浏览器自动化,这次升级能直接提升流程稳定性与迭代效率。
|
||||
@@ -76,7 +76,21 @@ flowchart TD
|
||||
|
||||
风险评分的关键不是“拟合某种固定画像”,而是“同一身份在多表面是否自洽”。
|
||||
|
||||
### 1.5 作用域绑定:hostname / action / cdata
|
||||
### 1.5 反调试脚本是独立攻击面,不等同于指纹命中
|
||||
|
||||
在高风控站点中,常见一类“主动防御脚本”(例如 `disable-devtool`):
|
||||
|
||||
1. 页面启动后检测开发者工具、控制台钩子、调试停顿特征
|
||||
2. 命中后执行主动处置(`window.close` / `history.back` / 跳转错误页)
|
||||
|
||||
这条链路的关键点:
|
||||
|
||||
- 它是“反调试执行面”,不是“指纹评分面”的简单子集
|
||||
- 会表现为“页面自关闭/自跳转”,但根因可能是运行时反调试触发
|
||||
|
||||
工程上必须将其与“指纹问题”分层处理,否则容易误判。
|
||||
|
||||
### 1.6 作用域绑定:hostname / action / cdata
|
||||
|
||||
服务端校验时提供用于绑定业务语义的字段:
|
||||
|
||||
@@ -96,7 +110,7 @@ flowchart LR
|
||||
D --> G["降低跨流程重放收益"]
|
||||
```
|
||||
|
||||
### 1.6 Token 状态机(能力令牌视角)
|
||||
### 1.7 Token 状态机(能力令牌视角)
|
||||
|
||||
从能力令牌视角,token 生命周期可抽象为:
|
||||
|
||||
@@ -114,7 +128,7 @@ stateDiagram-v2
|
||||
|
||||
设计目标是让“非法路径”快速失败,并且失败类型可被服务端语义区分。
|
||||
|
||||
### 1.7 攻击树(高层)
|
||||
### 1.8 攻击树(高层)
|
||||
|
||||
Turnstile 的主要攻击目标可以抽象为:
|
||||
|
||||
@@ -171,6 +185,12 @@ flowchart LR
|
||||
- “执行完整性”是上游条件
|
||||
- “信号修饰”是下游优化
|
||||
|
||||
补充:反调试脚本属于执行控制的一部分。
|
||||
|
||||
- 若页面含有主动防御脚本(如 `disable-devtool-auto` 入口),需要把“自动触发入口”与“正常业务脚本”分开处理
|
||||
- 最小化目标是阻断其自动化处置链路,避免误伤挑战脚本/跨域组件
|
||||
- 处理边界应是“只抑制高风险触发入口,不重写通用 DOM 语义”
|
||||
|
||||
### 2.3 服务端消费控制:把 token 当作能力消费
|
||||
|
||||
服务端消费控制的设计关键在于“放行条件定义”,而不是“接口调用细节”。
|
||||
@@ -228,7 +248,7 @@ Pre-clearance 通过后可产生 clearance,用于后续 WAF 挑战联动。
|
||||
Turnstile 攻防设计通常按以下优先级推进:
|
||||
|
||||
1. 服务端消费语义闭环(真实性 + 作用域 + 绑定 + 单次性 + 时效性)
|
||||
2. 挑战执行链路完整性(跨域语义保护)
|
||||
2. 挑战执行链路完整性(跨域语义保护 + 反调试触发面治理)
|
||||
3. 信号一致性(减少跨字段矛盾)
|
||||
4. 行为时序(降低机械分布)
|
||||
5. 体验优化(clearance 等摩擦控制)
|
||||
|
||||
@@ -131,12 +131,6 @@ This enables control of:
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--session <name></code>
|
||||
</td>
|
||||
<td>Use isolated session</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>-p <provider></code>
|
||||
|
||||
@@ -152,6 +152,9 @@ CDP mode uses a browser extension handshake to group tabs.
|
||||
- non-default: `Agent Browser Stealth • <session>`
|
||||
- Extension side panel (`agent-browser-stealth`) also provides:
|
||||
- Session window isolation and deterministic group colors.
|
||||
- Browser controls (`open`, `back`, `forward`, `reload`, `click/fill/press` by CSS selector).
|
||||
- Developer signals (console warnings/errors, fetch/xhr network events, DOM snapshot capture).
|
||||
- Workflow automation (record actions, run workflows, assign slash shortcuts, and create schedules).
|
||||
- `Keep Only This`, `Focus`, `Clean Empty Groups` quick actions.
|
||||
- Toggle switches for strict isolation / activation guard / auto-clean.
|
||||
- Session allowlist editing (domain fallback to `about:blank` when violated).
|
||||
@@ -290,8 +293,7 @@ agent-browser reload # Reload page
|
||||
## Global options
|
||||
|
||||
```bash
|
||||
--session <name> # Isolated browser session
|
||||
--session-name <name> # Auto-save/restore session state (defaults to --session when omitted)
|
||||
--session-name <name> # Auto-save/restore session state (defaults to "default" when omitted)
|
||||
--state <path> # Load storage state from JSON file
|
||||
--headers <json> # HTTP headers scoped to URL's origin
|
||||
--executable-path <path> # Custom browser executable
|
||||
|
||||
@@ -119,15 +119,6 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>session</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>--session</code>
|
||||
</td>
|
||||
<td>string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>sessionName</code>
|
||||
@@ -321,7 +312,7 @@ For tab grouping in CDP mode, grouping is best-effort through the extension hand
|
||||
extension available => grouped by session; extension missing/unavailable => silent no-op.
|
||||
|
||||
With the `agent-browser-stealth` extension installed, the side panel also exposes
|
||||
session window isolation controls, activation guard toggles, empty-group cleanup, and per-session allowlist policy editing.
|
||||
session window isolation controls, activation guard toggles, empty-group cleanup, per-session allowlist policy editing, browser action controls, developer signal panels (console/network/DOM), and workflow automation (recording, shortcuts, and scheduled runs).
|
||||
|
||||
## Common Configurations
|
||||
|
||||
@@ -474,11 +465,10 @@ These environment variables configure additional daemon and runtime behavior:
|
||||
<code>AGENT_BROWSER_SESSION_NAME</code>
|
||||
</td>
|
||||
<td>
|
||||
Auto-save/load state persistence name (defaults to <code>AGENT_BROWSER_SESSION</code> when
|
||||
unset).
|
||||
Auto-save/load state persistence name (defaults to <code>default</code> when unset).
|
||||
</td>
|
||||
<td>
|
||||
(same as <code>AGENT_BROWSER_SESSION</code>)
|
||||
<code>default</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
|
||||
export const metadata = pageMetadata("sessions")
|
||||
export const metadata = pageMetadata('sessions');
|
||||
|
||||
# Sessions
|
||||
|
||||
Run multiple isolated browser instances:
|
||||
Use one default runtime session and optional named persistence:
|
||||
|
||||
```bash
|
||||
# Different sessions
|
||||
agent-browser --session agent1 open site-a.com
|
||||
agent-browser --session agent2 open site-b.com
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn"
|
||||
|
||||
# List active sessions
|
||||
agent-browser session list
|
||||
# Output:
|
||||
# Active sessions:
|
||||
# -> default
|
||||
# agent1
|
||||
|
||||
# Show current session
|
||||
# Show current runtime session
|
||||
agent-browser session
|
||||
# Output: default
|
||||
|
||||
# Show active daemon sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
## Session isolation
|
||||
|
||||
Each session has its own:
|
||||
The runtime session is fixed to `default`. Use `--session-name` to isolate persisted state files per workflow.
|
||||
|
||||
- Browser instance
|
||||
- Cookies and storage
|
||||
- Navigation history
|
||||
- Cookies and storage snapshots
|
||||
- Authentication state
|
||||
- Saved state lifecycle
|
||||
|
||||
## Session persistence
|
||||
|
||||
@@ -50,7 +39,7 @@ export AGENT_BROWSER_SESSION_NAME=twitter
|
||||
agent-browser open twitter.com
|
||||
```
|
||||
|
||||
If `--session-name` is omitted, it defaults to `--session` (or `default`).
|
||||
If `--session-name` is omitted, it defaults to `default`.
|
||||
|
||||
State files are stored in `~/.agent-browser/sessions/` and automatically loaded on daemon start.
|
||||
|
||||
@@ -164,12 +153,29 @@ agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>AGENT_BROWSER_SESSION</code></td><td>Browser session ID (default: "default")</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption</td></tr>
|
||||
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete states older than N days (default: 30)</td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>AGENT_BROWSER_SESSION_NAME</code>
|
||||
</td>
|
||||
<td>Auto-save/load state persistence name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>AGENT_BROWSER_ENCRYPTION_KEY</code>
|
||||
</td>
|
||||
<td>64-char hex key for AES-256-GCM encryption</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code>
|
||||
</td>
|
||||
<td>Auto-delete states older than N days (default: 30)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,6 +2,316 @@
|
||||
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
|
||||
const RESPONSE_TYPE = 'AB_TAB_GROUP_RESPONSE';
|
||||
|
||||
const CONTENT_EVENT_TYPE = 'AB_CONTENT_EVENT';
|
||||
const CONTENT_EXECUTE_ACTION = 'AB_CONTENT_EXECUTE_ACTION';
|
||||
const CONTENT_GET_DOM_STATE = 'AB_CONTENT_GET_DOM_STATE';
|
||||
const CONTENT_PING = 'AB_CONTENT_PING';
|
||||
const PAGE_BRIDGE_EVENT = 'AB_PAGE_BRIDGE_EVENT';
|
||||
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
|
||||
|
||||
const mutationState = {
|
||||
total: 0,
|
||||
recent: [],
|
||||
observerReady: false,
|
||||
};
|
||||
|
||||
function pushMutationSummary(entry) {
|
||||
mutationState.total += 1;
|
||||
mutationState.recent.push({
|
||||
...entry,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (mutationState.recent.length > 40) {
|
||||
mutationState.recent.splice(0, mutationState.recent.length - 40);
|
||||
}
|
||||
}
|
||||
|
||||
function serializeValue(value, depth = 0) {
|
||||
if (value === null || typeof value === 'undefined') return value;
|
||||
if (typeof value === 'string') return value.slice(0, 300);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
||||
if (depth > 2) return '[depth-limit]';
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.slice(0, 10).map((item) => serializeValue(item, depth + 1));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const out = {};
|
||||
for (const [key, entry] of Object.entries(value).slice(0, 15)) {
|
||||
out[key] = serializeValue(entry, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return String(value).slice(0, 300);
|
||||
}
|
||||
|
||||
function sendRuntimeEvent(kind, payload) {
|
||||
try {
|
||||
chrome.runtime.sendMessage({
|
||||
type: CONTENT_EVENT_TYPE,
|
||||
kind,
|
||||
payload: serializeValue(payload),
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch {
|
||||
// Ignore runtime channel errors.
|
||||
}
|
||||
}
|
||||
|
||||
function getPageBridgeEnabled() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.storage.local.get([STORAGE_OPTIONS_KEY], (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const rawOptions = result?.[STORAGE_OPTIONS_KEY];
|
||||
resolve(Boolean(rawOptions && typeof rawOptions === 'object' && rawOptions.pageBridgeEnabled === true));
|
||||
});
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function installPageBridge() {
|
||||
// Receives events emitted by the injected page-world hook script.
|
||||
const bridgeListener = (event) => {
|
||||
if (event.source !== window) return;
|
||||
const data = event.data;
|
||||
if (!data || data.type !== PAGE_BRIDGE_EVENT) return;
|
||||
sendRuntimeEvent(data.kind || 'page-event', data.payload || {});
|
||||
};
|
||||
|
||||
window.addEventListener('message', bridgeListener);
|
||||
|
||||
const parent = document.documentElement || document.head || document.body;
|
||||
if (!parent) return;
|
||||
|
||||
if (!(await getPageBridgeEnabled())) {
|
||||
sendRuntimeEvent('lifecycle', {
|
||||
event: 'bridge-disabled-default',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Use external extension script instead of inline text to reduce CSP conflicts.
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('page-bridge.js');
|
||||
script.async = false;
|
||||
script.dataset.abBridgeEvent = PAGE_BRIDGE_EVENT;
|
||||
script.onload = () => script.remove();
|
||||
script.onerror = () => {
|
||||
sendRuntimeEvent('lifecycle', {
|
||||
event: 'bridge-load-failed',
|
||||
host: window.location.hostname,
|
||||
});
|
||||
script.remove();
|
||||
};
|
||||
parent.appendChild(script);
|
||||
}
|
||||
|
||||
function ensureMutationObserver() {
|
||||
if (mutationState.observerReady) return;
|
||||
if (!document.documentElement) return;
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
const summary = {
|
||||
records: records.length,
|
||||
addedNodes: 0,
|
||||
removedNodes: 0,
|
||||
};
|
||||
|
||||
for (const record of records.slice(0, 40)) {
|
||||
summary.addedNodes += record.addedNodes?.length || 0;
|
||||
summary.removedNodes += record.removedNodes?.length || 0;
|
||||
}
|
||||
|
||||
pushMutationSummary(summary);
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style', 'hidden', 'disabled', 'aria-hidden'],
|
||||
});
|
||||
|
||||
mutationState.observerReady = true;
|
||||
}
|
||||
|
||||
function toSimpleNode(element) {
|
||||
if (!element || typeof element !== 'object') return null;
|
||||
const node = {
|
||||
tag: element.tagName?.toLowerCase() || 'unknown',
|
||||
id: element.id || undefined,
|
||||
className: typeof element.className === 'string' ? element.className.slice(0, 120) : '',
|
||||
role: element.getAttribute?.('role') || undefined,
|
||||
name:
|
||||
element.getAttribute?.('aria-label') ||
|
||||
element.getAttribute?.('name') ||
|
||||
element.getAttribute?.('placeholder') ||
|
||||
'',
|
||||
text: (element.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 160),
|
||||
disabled: element.disabled === true,
|
||||
hidden: element.hidden === true,
|
||||
};
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function collectInteractiveElements(root, limit = 80) {
|
||||
const selector = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input',
|
||||
'select',
|
||||
'textarea',
|
||||
'summary',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[tabindex]'
|
||||
].join(',');
|
||||
|
||||
const out = [];
|
||||
const nodes = root.querySelectorAll(selector);
|
||||
for (const element of nodes) {
|
||||
if (out.length >= limit) break;
|
||||
out.push(toSimpleNode(element));
|
||||
}
|
||||
return out.filter(Boolean);
|
||||
}
|
||||
|
||||
function collectDomState(options = {}) {
|
||||
const selector = typeof options.selector === 'string' ? options.selector.trim() : '';
|
||||
const root = selector ? document.querySelector(selector) : document.body || document.documentElement;
|
||||
|
||||
if (!root) {
|
||||
return {
|
||||
ok: false,
|
||||
error: selector ? `selector-not-found: ${selector}` : 'root-not-found',
|
||||
};
|
||||
}
|
||||
|
||||
const textPreview = (root.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 1000);
|
||||
const interactiveOnly = options.interactiveOnly === true;
|
||||
const interactiveElements = collectInteractiveElements(root, options.maxNodes || 80);
|
||||
|
||||
const dom = {
|
||||
href: window.location.href,
|
||||
title: document.title,
|
||||
readyState: document.readyState,
|
||||
selector: selector || null,
|
||||
rootTag: root.tagName?.toLowerCase() || 'unknown',
|
||||
textPreview,
|
||||
interactiveCount: interactiveElements.length,
|
||||
interactiveElements,
|
||||
mutation: {
|
||||
total: mutationState.total,
|
||||
recent: mutationState.recent.slice(-10),
|
||||
},
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
|
||||
if (interactiveOnly) {
|
||||
dom.textPreview = '';
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
state: dom,
|
||||
};
|
||||
}
|
||||
|
||||
function queryElement(selector) {
|
||||
if (typeof selector !== 'string' || selector.trim().length === 0) {
|
||||
throw new Error('selector is required');
|
||||
}
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${selector}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function focusElement(element) {
|
||||
if (typeof element.focus === 'function') {
|
||||
element.focus({ preventScroll: false });
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
async function executeAction(command, args = {}) {
|
||||
switch (command) {
|
||||
case 'click': {
|
||||
const element = queryElement(args.selector);
|
||||
focusElement(element);
|
||||
element.click();
|
||||
return { ok: true, action: command, selector: args.selector };
|
||||
}
|
||||
case 'fill': {
|
||||
const element = queryElement(args.selector);
|
||||
if (!('value' in element)) {
|
||||
throw new Error(`Element is not fillable: ${args.selector}`);
|
||||
}
|
||||
focusElement(element);
|
||||
element.value = typeof args.value === 'string' ? args.value : String(args.value || '');
|
||||
dispatchInputEvents(element);
|
||||
return { ok: true, action: command, selector: args.selector, valueLength: element.value.length };
|
||||
}
|
||||
case 'press': {
|
||||
const key = typeof args.key === 'string' && args.key.trim().length > 0 ? args.key.trim() : 'Enter';
|
||||
let target;
|
||||
if (typeof args.selector === 'string' && args.selector.trim().length > 0) {
|
||||
target = queryElement(args.selector);
|
||||
focusElement(target);
|
||||
} else {
|
||||
target = document.activeElement || document.body;
|
||||
}
|
||||
|
||||
const down = new KeyboardEvent('keydown', { key, bubbles: true });
|
||||
const up = new KeyboardEvent('keyup', { key, bubbles: true });
|
||||
target.dispatchEvent(down);
|
||||
target.dispatchEvent(up);
|
||||
return { ok: true, action: command, key };
|
||||
}
|
||||
case 'eval': {
|
||||
if (typeof args.expression !== 'string' || args.expression.trim().length === 0) {
|
||||
throw new Error('expression is required');
|
||||
}
|
||||
const fn = new Function(`return (${args.expression});`);
|
||||
const result = fn();
|
||||
return { ok: true, action: command, result: serializeValue(result) };
|
||||
}
|
||||
case 'snapshot': {
|
||||
return {
|
||||
ok: true,
|
||||
action: command,
|
||||
...collectDomState({
|
||||
selector: args.selector,
|
||||
interactiveOnly: args.interactiveOnly === true,
|
||||
maxNodes: args.maxNodes,
|
||||
}),
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown content action: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureMutationObserver();
|
||||
installPageBridge();
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
@@ -80,4 +390,36 @@
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (!message || typeof message !== 'object') return;
|
||||
|
||||
if (message.type === CONTENT_PING) {
|
||||
sendResponse({
|
||||
ok: true,
|
||||
href: window.location.href,
|
||||
title: document.title,
|
||||
readyState: document.readyState,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === CONTENT_GET_DOM_STATE) {
|
||||
sendResponse(collectDomState(message.options || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === CONTENT_EXECUTE_ACTION) {
|
||||
executeAction(message.command, message.args || {})
|
||||
.then((result) => sendResponse(result))
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
action: message.command,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="32" fill="#1A73E8"/>
|
||||
<rect x="30" y="34" width="68" height="10" rx="2" fill="white"/>
|
||||
<rect x="30" y="54" width="48" height="10" rx="2" fill="white" fill-opacity="0.8"/>
|
||||
<rect x="30" y="74" width="28" height="10" rx="2" fill="white" fill-opacity="0.6"/>
|
||||
<circle cx="94" cy="90" r="10" fill="#34A853" stroke="#1A73E8" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
@@ -3,6 +3,9 @@
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.2.0",
|
||||
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
|
||||
"icons": {
|
||||
"128": "icons/icon.svg"
|
||||
},
|
||||
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel", "alarms"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
@@ -21,5 +24,11 @@
|
||||
"run_at": "document_start",
|
||||
"match_about_blank": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["page-bridge.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
(() => {
|
||||
if (window.__AB_STEALTH_BRIDGE_INSTALLED__) return;
|
||||
window.__AB_STEALTH_BRIDGE_INSTALLED__ = true;
|
||||
|
||||
const currentScript = document.currentScript;
|
||||
const TYPE = currentScript?.dataset?.abBridgeEvent || 'AB_PAGE_BRIDGE_EVENT';
|
||||
|
||||
const post = (kind, payload) => {
|
||||
try {
|
||||
window.postMessage({ type: TYPE, kind, payload, timestamp: Date.now() }, '*');
|
||||
} catch {
|
||||
// Ignore post failures.
|
||||
}
|
||||
};
|
||||
|
||||
const serializeArg = (value, depth = 0) => {
|
||||
if (value === null || typeof value === 'undefined') return value;
|
||||
if (typeof value === 'string') return value.slice(0, 250);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
||||
if (depth > 2) return '[depth-limit]';
|
||||
if (Array.isArray(value)) return value.slice(0, 10).map((item) => serializeArg(item, depth + 1));
|
||||
if (typeof value === 'object') {
|
||||
const out = {};
|
||||
const entries = Object.entries(value).slice(0, 12);
|
||||
for (const [k, v] of entries) {
|
||||
out[k] = serializeArg(v, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return String(value).slice(0, 250);
|
||||
};
|
||||
|
||||
const patchConsoleMethod = (name) => {
|
||||
const original = console[name];
|
||||
if (typeof original !== 'function') return;
|
||||
console[name] = function patchedConsole(...args) {
|
||||
post('console', {
|
||||
level: name,
|
||||
args: args.map((arg) => serializeArg(arg)),
|
||||
});
|
||||
return original.apply(this, args);
|
||||
};
|
||||
};
|
||||
|
||||
patchConsoleMethod('error');
|
||||
patchConsoleMethod('warn');
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
post('console', {
|
||||
level: 'error',
|
||||
message: event.message,
|
||||
source: event.filename,
|
||||
line: event.lineno,
|
||||
column: event.colno,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
post('console', {
|
||||
level: 'error',
|
||||
message: 'Unhandled rejection',
|
||||
reason: serializeArg(event.reason),
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof window.fetch === 'function') {
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = async (...args) => {
|
||||
const startedAt = Date.now();
|
||||
const requestInfo = args[0];
|
||||
const requestInit = args[1] || {};
|
||||
const method = requestInit.method || 'GET';
|
||||
const url = typeof requestInfo === 'string' ? requestInfo : requestInfo?.url || '';
|
||||
|
||||
try {
|
||||
const response = await originalFetch(...args);
|
||||
post('network', {
|
||||
transport: 'fetch',
|
||||
method,
|
||||
url,
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
post('network', {
|
||||
transport: 'fetch',
|
||||
method,
|
||||
url,
|
||||
error: serializeArg(error),
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.XMLHttpRequest === 'function') {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
|
||||
this.__abRequestMeta = {
|
||||
method: typeof method === 'string' ? method : 'GET',
|
||||
url: typeof url === 'string' ? url : String(url || ''),
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
return originalOpen.call(this, method, url, ...rest);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function patchedSend(...args) {
|
||||
this.addEventListener('loadend', () => {
|
||||
const meta = this.__abRequestMeta || {};
|
||||
post('network', {
|
||||
transport: 'xhr',
|
||||
method: meta.method || 'GET',
|
||||
url: meta.url || '',
|
||||
status: this.status,
|
||||
ok: this.status >= 200 && this.status < 400,
|
||||
durationMs: Date.now() - (meta.startedAt || Date.now()),
|
||||
});
|
||||
});
|
||||
return originalSend.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
post('lifecycle', {
|
||||
event: 'bridge-installed',
|
||||
href: location.href,
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,66 @@
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--panel: #161b22;
|
||||
--item-bg: rgba(255, 255, 255, 0.03);
|
||||
--item-hover: rgba(255, 255, 255, 0.06);
|
||||
--border: #30363d;
|
||||
--border-focus: #484f58;
|
||||
--text-main: #e6edf3;
|
||||
--text-muted: #848d97;
|
||||
--accent: #2f81f7;
|
||||
--accent-soft: rgba(47, 129, 247, 0.1);
|
||||
--success: #3fb950;
|
||||
--warning: #d29922;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
--bg: #f3f5f7;
|
||||
--surface: #ffffff;
|
||||
--surface-alt: #f6f8fb;
|
||||
--primary: #1769e0;
|
||||
--primary-hover: #0f58c0;
|
||||
--border: #d8dde4;
|
||||
--text-main: #18212f;
|
||||
--text-secondary: #4a5568;
|
||||
--text-muted: #667287;
|
||||
--success: #117a3d;
|
||||
--warning: #b86d00;
|
||||
--danger: #bd1e24;
|
||||
--radius: 10px;
|
||||
--mono: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: var(--bg);
|
||||
padding: 14px;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-family);
|
||||
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--border-focus); }
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 19px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
h1::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--accent);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -71,169 +73,186 @@ button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-main);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
background: var(--surface);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--item-hover);
|
||||
border-color: var(--border-focus);
|
||||
border-color: var(--primary);
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
#refresh-btn {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #ffffff;
|
||||
button.primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#refresh-btn:hover {
|
||||
background: #4493f8;
|
||||
border-color: #4493f8;
|
||||
button.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
button.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
h3 {
|
||||
.status-line {
|
||||
min-height: 18px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-line.ok {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-line.warn {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.status-line.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row + .row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 7px 9px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-alt);
|
||||
color: var(--text-main);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input.mono,
|
||||
textarea.mono,
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #0f172a;
|
||||
color: #dce6fb;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.session-title h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border: 1px solid transparent;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px; /* Divider effect */
|
||||
background: var(--border);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item {
|
||||
background: var(--panel);
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--item-hover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.item-url {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.row-actions button {
|
||||
padding: 4px 8px;
|
||||
.caption {
|
||||
font-size: 11px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
background: rgba(110, 118, 129, 0.4);
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
#summary code {
|
||||
color: var(--warning);
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Summary Grid */
|
||||
#summary .tags {
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#summary .tag {
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
.event-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 7px;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
background: #f0f4fa;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
<button id="refresh-btn" type="button">Refresh</button>
|
||||
<button id="cleanup-btn" type="button">Clean Empty Groups</button>
|
||||
</div>
|
||||
<div id="status-line" class="status-line"></div>
|
||||
</header>
|
||||
|
||||
<section id="control" class="card"></section>
|
||||
<section id="summary" class="card"></section>
|
||||
<section id="automation" class="card"></section>
|
||||
<section id="developer" class="card"></section>
|
||||
<section id="sessions" class="stack"></section>
|
||||
<section id="downloads" class="card"></section>
|
||||
|
||||
|
||||
+1119
-140
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.16.1-fork.2",
|
||||
"version": "0.16.3-fork.2",
|
||||
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
|
||||
"type": "module",
|
||||
"main": "dist/daemon.js",
|
||||
@@ -42,12 +42,15 @@
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"verify:native-version": "node scripts/verify-native-version.js",
|
||||
"verify:bundled-binaries": "node scripts/verify-bundled-binaries.js",
|
||||
"verify:packed-host-binary": "node scripts/verify-packed-host-binary.js",
|
||||
"verify:registry-host-binary": "node scripts/verify-registry-host-binary.js",
|
||||
"prepublishOnly": "pnpm run verify:bundled-binaries && pnpm run verify:native-version && pnpm run verify:packed-host-binary",
|
||||
"clawhub:sync": "bash scripts/clawhub-sync.sh",
|
||||
"sync:upstream": "bash scripts/sync-upstream.sh",
|
||||
"sync:upstream:push": "bash scripts/sync-upstream.sh --push",
|
||||
"changeset": "changeset",
|
||||
"ci:version": "changeset version && pnpm run version:sync && pnpm install --no-frozen-lockfile",
|
||||
"ci:publish": "pnpm run version:sync && pnpm run build && pnpm run build:native && pnpm run verify:bundled-binaries && pnpm run verify:native-version && changeset publish"
|
||||
"ci:publish": "pnpm run version:sync && pnpm run build && pnpm run build:native && pnpm run verify:bundled-binaries && pnpm run verify:native-version && pnpm run verify:packed-host-binary && changeset publish"
|
||||
},
|
||||
"keywords": [
|
||||
"browser",
|
||||
|
||||
+52
-16
@@ -2,19 +2,29 @@
|
||||
|
||||
/**
|
||||
* Postinstall script for agent-browser
|
||||
*
|
||||
*
|
||||
* Downloads the platform-specific native binary if not present.
|
||||
* On global installs, patches npm's bin entry to use the native binary directly:
|
||||
* - Windows: Overwrites .cmd/.ps1 shims
|
||||
* - Mac/Linux: Replaces symlink to point to native binary
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync, readFileSync } from 'fs';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
chmodSync,
|
||||
createWriteStream,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
symlinkSync,
|
||||
lstatSync,
|
||||
readFileSync,
|
||||
} from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { platform, arch } from 'os';
|
||||
import { get } from 'https';
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync, execSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
@@ -65,7 +75,7 @@ function getBinCommands(pkg) {
|
||||
async function downloadFile(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
|
||||
const request = (url) => {
|
||||
get(url, (response) => {
|
||||
// Handle redirects
|
||||
@@ -73,12 +83,12 @@ async function downloadFile(url, dest) {
|
||||
request(response.headers.location);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
@@ -89,7 +99,7 @@ async function downloadFile(url, dest) {
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
request(url);
|
||||
});
|
||||
}
|
||||
@@ -101,13 +111,21 @@ async function main() {
|
||||
if (platform() !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
console.log(`✓ Native binary ready: ${binaryName}`);
|
||||
|
||||
// On global installs, fix npm's bin entry to use native binary directly
|
||||
await fixGlobalInstallBin();
|
||||
|
||||
showPlaywrightReminder();
|
||||
return;
|
||||
const installedVersion = readBinaryVersion(binaryPath);
|
||||
if (installedVersion.includes(version)) {
|
||||
console.log(`✓ Native binary ready: ${binaryName}`);
|
||||
|
||||
// On global installs, fix npm's bin entry to use native binary directly
|
||||
await fixGlobalInstallBin();
|
||||
|
||||
showPlaywrightReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`⚠ Binary version mismatch detected: expected ${version}, got "${installedVersion || 'unknown'}"`
|
||||
);
|
||||
console.log(` Re-downloading ${binaryName} from release assets...`);
|
||||
}
|
||||
|
||||
// Ensure bin directory exists
|
||||
@@ -120,12 +138,19 @@ async function main() {
|
||||
|
||||
try {
|
||||
await downloadFile(DOWNLOAD_URL, binaryPath);
|
||||
|
||||
|
||||
// Make executable on Unix
|
||||
if (platform() !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
|
||||
const downloadedVersion = readBinaryVersion(binaryPath);
|
||||
if (!downloadedVersion.includes(version)) {
|
||||
throw new Error(
|
||||
`downloaded binary version mismatch: expected ${version}, got "${downloadedVersion || 'unknown'}"`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✓ Downloaded native binary: ${binaryName}`);
|
||||
} catch (err) {
|
||||
console.log(`⚠ Could not download native binary: ${err.message}`);
|
||||
@@ -143,6 +168,17 @@ async function main() {
|
||||
showPlaywrightReminder();
|
||||
}
|
||||
|
||||
function readBinaryVersion(path) {
|
||||
try {
|
||||
return execFileSync(path, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function showPlaywrightReminder() {
|
||||
console.log('');
|
||||
console.log('╔═══════════════════════════════════════════════════════════════════════════╗');
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build an npm tarball locally and verify that the bundled host-platform native
|
||||
* binary reports the same version as package.json.
|
||||
*
|
||||
* This catches publish drifts where package.json is bumped but the embedded
|
||||
* binary still points to an older fork version.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'));
|
||||
const expectedVersion = String(pkg.version || '').trim();
|
||||
|
||||
if (!expectedVersion) {
|
||||
console.error('Error: package.json version is empty');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const hostBinaryName = `agent-browser-${process.platform}-${process.arch}${ext}`;
|
||||
|
||||
let tempDir = '';
|
||||
let packedTarball = '';
|
||||
|
||||
try {
|
||||
const packRaw = execFileSync('npm', ['pack', '--json'], {
|
||||
cwd: rootDir,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const parsed = JSON.parse(packRaw);
|
||||
const filename = parsed?.[0]?.filename;
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
throw new Error(`unexpected npm pack output: ${packRaw}`);
|
||||
}
|
||||
|
||||
packedTarball = join(rootDir, filename);
|
||||
if (!existsSync(packedTarball)) {
|
||||
throw new Error(`tarball not found: ${packedTarball}`);
|
||||
}
|
||||
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ab-pack-verify-'));
|
||||
execFileSync('tar', ['-xzf', packedTarball, '-C', tempDir], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const packedRoot = join(tempDir, 'package');
|
||||
const packedPkg = JSON.parse(readFileSync(join(packedRoot, 'package.json'), 'utf8'));
|
||||
if (String(packedPkg.version || '').trim() !== expectedVersion) {
|
||||
throw new Error(
|
||||
`packed package.json version mismatch: expected ${expectedVersion}, got ${packedPkg.version}`
|
||||
);
|
||||
}
|
||||
|
||||
const packedBinary = join(packedRoot, 'bin', hostBinaryName);
|
||||
if (!existsSync(packedBinary)) {
|
||||
throw new Error(`host binary missing in tarball: bin/${hostBinaryName}`);
|
||||
}
|
||||
|
||||
const binaryVersion = execFileSync(packedBinary, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
|
||||
if (!binaryVersion.includes(expectedVersion)) {
|
||||
throw new Error(
|
||||
`tarball host binary version mismatch: expected ${expectedVersion}, got "${binaryVersion}"`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✓ Packed tarball host binary matches package.json (${expectedVersion})`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: ${message}`);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
||||
if (packedTarball && existsSync(packedTarball)) unlinkSync(packedTarball);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Download the published npm tarball and verify that the bundled host-platform
|
||||
* native binary reports the expected package version.
|
||||
*/
|
||||
|
||||
import { createWriteStream, existsSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { get } from 'https';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'));
|
||||
const packageName = process.env.PACKAGE_NAME || pkg.name || 'agent-browser-stealth';
|
||||
const expectedVersion = process.env.EXPECTED_VERSION || pkg.version;
|
||||
|
||||
if (!expectedVersion) {
|
||||
console.error('Error: expected version is empty');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const hostBinaryName = `agent-browser-${process.platform}-${process.arch}${ext}`;
|
||||
|
||||
function downloadFile(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
const request = (currentUrl) => {
|
||||
get(currentUrl, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
request(response.headers.location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download tarball: HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
request(url);
|
||||
});
|
||||
}
|
||||
|
||||
let tempDir = '';
|
||||
let tarballPath = '';
|
||||
|
||||
try {
|
||||
const tarballUrl = execFileSync(
|
||||
'npm',
|
||||
['view', `${packageName}@${expectedVersion}`, 'dist.tarball'],
|
||||
{
|
||||
cwd: rootDir,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}
|
||||
).trim();
|
||||
|
||||
if (!tarballUrl) {
|
||||
throw new Error(`could not resolve dist.tarball for ${packageName}@${expectedVersion}`);
|
||||
}
|
||||
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ab-registry-verify-'));
|
||||
tarballPath = join(tempDir, 'package.tgz');
|
||||
await downloadFile(tarballUrl, tarballPath);
|
||||
|
||||
execFileSync('tar', ['-xzf', tarballPath, '-C', tempDir], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const packedRoot = join(tempDir, 'package');
|
||||
const packedPkg = JSON.parse(readFileSync(join(packedRoot, 'package.json'), 'utf8'));
|
||||
if (String(packedPkg.version || '').trim() !== expectedVersion) {
|
||||
throw new Error(
|
||||
`registry package.json version mismatch: expected ${expectedVersion}, got ${packedPkg.version}`
|
||||
);
|
||||
}
|
||||
|
||||
const packedBinary = join(packedRoot, 'bin', hostBinaryName);
|
||||
if (!existsSync(packedBinary)) {
|
||||
throw new Error(`host binary missing in registry tarball: bin/${hostBinaryName}`);
|
||||
}
|
||||
|
||||
const binaryVersion = execFileSync(packedBinary, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
|
||||
if (!binaryVersion.includes(expectedVersion)) {
|
||||
throw new Error(
|
||||
`registry host binary version mismatch: expected ${expectedVersion}, got "${binaryVersion}"`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✓ Registry tarball host binary matches package.json (${packageName}@${expectedVersion})`
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: ${message}`);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (tarballPath && existsSync(tarballPath)) unlinkSync(tarballPath);
|
||||
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -173,7 +173,7 @@ agent-browser cookies set callback_token "token123"
|
||||
|
||||
```bash
|
||||
# Auto-save/restore cookies and localStorage across browser restarts
|
||||
# If --session-name is omitted, it defaults to --session (or "default")
|
||||
# If --session-name is omitted, it defaults to "default"
|
||||
agent-browser --session-name myapp open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
agent-browser close # State auto-saved to ~/.agent-browser/sessions/
|
||||
@@ -205,16 +205,14 @@ agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
### Parallel Sessions
|
||||
### Parallel Workflows
|
||||
|
||||
```bash
|
||||
agent-browser --session site1 open https://site-a.com
|
||||
agent-browser --session site2 open https://site-b.com
|
||||
agent-browser --session-name site1 open https://site-a.com
|
||||
agent-browser --session-name site2 open https://site-b.com
|
||||
|
||||
agent-browser --session site1 snapshot -i
|
||||
agent-browser --session site2 snapshot -i
|
||||
|
||||
agent-browser session list
|
||||
agent-browser --session-name site1 snapshot -i
|
||||
agent-browser --session-name site2 snapshot -i
|
||||
```
|
||||
|
||||
### Connect to Existing Chrome
|
||||
@@ -286,7 +284,10 @@ Notes:
|
||||
- non-default session: `Agent Browser Stealth • <session>`
|
||||
- Additional extension-side capabilities:
|
||||
- Session window isolation + deterministic group colors.
|
||||
- Side panel controls: Focus / Keep Only This / Clean Empty Groups + isolation/auto-clean toggles.
|
||||
- Side panel browser controls: `open` / `back` / `forward` / `reload` + selector-based `click` / `fill` / `press`.
|
||||
- Side panel developer signals: page console events, fetch/xhr network events, command history, and on-demand DOM snapshots.
|
||||
- Side panel automation: action recording, workflow run, slash shortcut binding, and scheduled execution (daily/weekly/monthly/yearly).
|
||||
- Side panel operations: Focus / Keep Only This / Clean Empty Groups + isolation/auto-clean toggles.
|
||||
- Session allowlist policy editing and fallback blocking (`about:blank`).
|
||||
- Download auto-routing to `agent-browser-stealth/<session>/...`.
|
||||
|
||||
@@ -300,6 +301,8 @@ agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop trace.json # Stop and save profile (path optional)
|
||||
```
|
||||
|
||||
Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode.
|
||||
|
||||
### Local Files (PDFs, HTML)
|
||||
|
||||
```bash
|
||||
@@ -486,22 +489,12 @@ These behaviors are always active. For sensitive sites, combine with `--headed`
|
||||
|
||||
## Session Management and Cleanup
|
||||
|
||||
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
|
||||
|
||||
```bash
|
||||
# Each agent gets its own isolated session
|
||||
agent-browser --session agent1 open site-a.com
|
||||
agent-browser --session agent2 open site-b.com
|
||||
|
||||
# Check active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
`--session` is ignored in this fork. The runtime always uses one default session. Use `--session-name` to isolate persistence when needed.
|
||||
|
||||
Always close your browser session when done to avoid leaked processes:
|
||||
|
||||
```bash
|
||||
agent-browser close # Close default session
|
||||
agent-browser --session agent1 close # Close specific session
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.
|
||||
|
||||
+1
-1
@@ -2135,7 +2135,7 @@ export class BrowserManager {
|
||||
context = await launcher.launchPersistentContext(
|
||||
path.join(os.tmpdir(), `agent-browser-ext-${session}`),
|
||||
{
|
||||
headless: false,
|
||||
headless: options.headless ?? false,
|
||||
executablePath: options.executablePath,
|
||||
...(chromeChannel && { channel: chromeChannel }),
|
||||
args: allArgs,
|
||||
|
||||
+4
-2
@@ -432,7 +432,7 @@ export async function startDaemon(options?: {
|
||||
} else if (manager instanceof BrowserManager) {
|
||||
// Auto-launch desktop browser
|
||||
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
|
||||
? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
|
||||
? process.env.AGENT_BROWSER_EXTENSIONS.split(/[,\n]/)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
@@ -471,7 +471,9 @@ export async function startDaemon(options?: {
|
||||
const launchOptions = {
|
||||
id: 'auto',
|
||||
action: 'launch' as const,
|
||||
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
||||
headless:
|
||||
process.env.AGENT_BROWSER_HEADED !== '1' &&
|
||||
process.env.AGENT_BROWSER_HEADED !== 'true',
|
||||
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
||||
extensions: extensions,
|
||||
storageState: process.env.AGENT_BROWSER_STATE,
|
||||
|
||||
@@ -126,6 +126,34 @@ describe('Stealth mode', () => {
|
||||
expect(signals.inNavigator).toBe(false);
|
||||
});
|
||||
|
||||
it('hides disable-devtool auto bootstrap selector while keeping normal selectors intact', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const signals = await browser.getPage().evaluate(() => {
|
||||
const marker = document.createElement('meta');
|
||||
marker.setAttribute('data-ab-marker', 'ok');
|
||||
document.head.appendChild(marker);
|
||||
|
||||
const dd = document.createElement('script');
|
||||
dd.setAttribute('disable-devtool-auto', '');
|
||||
document.head.appendChild(dd);
|
||||
|
||||
const hiddenOne = document.querySelector('[disable-devtool-auto]') === null;
|
||||
const hiddenCount = document.querySelectorAll('[disable-devtool-auto]').length;
|
||||
const normalSelectorWorks = !!document.querySelector('[data-ab-marker="ok"]');
|
||||
|
||||
dd.remove();
|
||||
marker.remove();
|
||||
|
||||
return { hiddenOne, hiddenCount, normalSelectorWorks };
|
||||
});
|
||||
|
||||
expect(signals.hiddenOne).toBe(true);
|
||||
expect(signals.hiddenCount).toBe(0);
|
||||
expect(signals.normalSelectorWorks).toBe(true);
|
||||
});
|
||||
|
||||
it('neutralizes creepjs prefers-color-scheme light probe', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
@@ -292,6 +292,7 @@ function buildStealthScript(options: StealthScriptOptions): string {
|
||||
configScript,
|
||||
patchNavigatorWebdriver(),
|
||||
patchCssSupportsWebdriverHeuristic(),
|
||||
patchDisableDevtoolAutoBootstrap(),
|
||||
patchChromeRuntime(),
|
||||
patchChromeLegacyApis(),
|
||||
patchIframeContentWindow(),
|
||||
@@ -366,6 +367,65 @@ function patchCssSupportsWebdriverHeuristic(): string {
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some high-risk sites ship disable-devtool with auto bootstrap via
|
||||
* document.querySelector('[disable-devtool-auto]').
|
||||
*
|
||||
* Hiding only this selector prevents the default self-close/redirect behavior
|
||||
* without affecting normal selector queries.
|
||||
*/
|
||||
function patchDisableDevtoolAutoBootstrap(): string {
|
||||
return `(function(){
|
||||
if (typeof Document === 'undefined') return;
|
||||
const AUTO_SELECTOR = '[disable-devtool-auto]';
|
||||
const NEVER_MATCH_SELECTOR = 'script[__ab_disable_devtool_never_match__="1"]';
|
||||
const normalize = (value) => {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value.replace(/\\s+/g, '').toLowerCase();
|
||||
};
|
||||
const shouldHideSelector = (selector) => normalize(selector) === AUTO_SELECTOR;
|
||||
|
||||
const patchQueryMethod = (proto, method) => {
|
||||
if (!proto) return;
|
||||
const native = proto[method];
|
||||
if (typeof native !== 'function') return;
|
||||
|
||||
const wrapped = function(selector, ...args) {
|
||||
if (shouldHideSelector(selector)) {
|
||||
return native.call(this, NEVER_MATCH_SELECTOR, ...args);
|
||||
}
|
||||
return native.call(this, selector, ...args);
|
||||
};
|
||||
|
||||
try {
|
||||
Object.defineProperty(wrapped, 'name', {
|
||||
value: native.name,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(wrapped, 'toString', {
|
||||
value: () => native.toString(),
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
Object.defineProperty(proto, method, {
|
||||
value: wrapped,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
patchQueryMethod(Document.prototype, 'querySelector');
|
||||
patchQueryMethod(Document.prototype, 'querySelectorAll');
|
||||
if (typeof Element !== 'undefined') {
|
||||
patchQueryMethod(Element.prototype, 'querySelector');
|
||||
patchQueryMethod(Element.prototype, 'querySelectorAll');
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove navigator.webdriver entirely.
|
||||
* Modern detection checks both value and property presence (`'webdriver' in navigator`).
|
||||
@@ -1268,6 +1328,19 @@ function patchNavigatorConnection(): string {
|
||||
function patchWorkerConnection(): string {
|
||||
return `(function(){
|
||||
if (typeof Worker !== 'function') return;
|
||||
const isCloudflareChallengeRuntime = (() => {
|
||||
try {
|
||||
const host = String(location.hostname || '').toLowerCase();
|
||||
const path = String(location.pathname || '');
|
||||
if (host === 'challenges.cloudflare.com') return true;
|
||||
return /\\/cdn-cgi\\/challenge-platform\\//.test(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
// Cloudflare challenge workers are sensitive to constructor wrapping.
|
||||
// Keep native Worker behavior in this runtime to avoid importScripts(blob) failures.
|
||||
if (isCloudflareChallengeRuntime) return;
|
||||
const NativeWorker = Worker;
|
||||
const workerPrelude = \`
|
||||
(() => {
|
||||
|
||||
Reference in New Issue
Block a user