Skip to content

Commit

Permalink
Docs: improve discoverability of image compression (#5675)
Browse files Browse the repository at this point in the history
### What
We often talk to customers that are unaware that we support logging
encoded/compressed images.
So I added some docs for this (in 3096c17).

This became a 🐰🕳️ because the doc tags (`\py` etc) didn't work as
expected: they would put all language-specific stuff _at the end_, but
in this case I wanted it interleaved into the rest of the docs. So I
fixed that, with a lot of effort.
Now `\cpp`, `\py`, `\rs` acts like a simple per-line filter.

In the meantime I removed some unused code, making everything shorter in
the process.

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[app.rerun.io](https://app.rerun.io/pr/5675/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/5675/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[app.rerun.io](https://app.rerun.io/pr/5675/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5675)
- [Docs
preview](https://rerun.io/preview/5e9c4730e352b2fa9ed32406937e317552387660/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/5e9c4730e352b2fa9ed32406937e317552387660/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
emilk authored Mar 26, 2024
1 parent 4280fd4 commit e5ebdf6
Show file tree
Hide file tree
Showing 20 changed files with 250 additions and 303 deletions.
8 changes: 7 additions & 1 deletion crates/re_types/definitions/rerun/archetypes/image.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ namespace rerun.archetypes;
/// Leading and trailing unit-dimensions are ignored, so that
/// `1x640x480x3x1` is treated as a `640x480x3` RGB image.
///
/// Rerun also supports compressed image encoded as JPEG, N12, and YUY2.
/// Using these formats can save a lot of bandwidth and memory.
/// \py To compress an image, use [`rerun.Image.compress`][].
/// \py To pass in an already encoded image, use [`rerun.ImageEncoded`][].
/// \rs See [`crate::components::TensorData`] for more.
/// \cpp See [`rerun::datatypes::TensorBuffer`] for more.
///
/// \cpp Since the underlying `rerun::datatypes::TensorData` uses `rerun::Collection` internally,
/// \cpp data can be passed in without a copy from raw pointers or by reference from `std::vector`/`std::array`/c-arrays.
/// \cpp If needed, this "borrow-behavior" can be extended by defining your own `rerun::CollectionAdapter`.
/// \python For an easy way to pass in image formats or encoded images, see [`rerun.ImageEncoded`][].
///
/// \example image_simple image="https://static.rerun.io/image_simple/06ba7f8582acc1ffb42a7fd0006fad7816f3e4e4/1200w.png"
table Image (
Expand Down
4 changes: 2 additions & 2 deletions crates/re_types/definitions/rerun/archetypes/points2d.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ table Points2D (

/// Optional colors for the points.
///
/// \python The colors are interpreted as RGB or RGBA in sRGB gamma-space,
/// \python As either 0-1 floats or 0-255 integers, with separate alpha.
/// \py The colors are interpreted as RGB or RGBA in sRGB gamma-space,
/// \py As either 0-1 floats or 0-255 integers, with separate alpha.
colors: [rerun.components.Color] ("attr.rerun.component_recommended", nullable, order: 2100);

// --- Optional ---
Expand Down
4 changes: 2 additions & 2 deletions crates/re_types/definitions/rerun/archetypes/points3d.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ table Points3D (

/// Optional colors for the points.
///
/// \python The colors are interpreted as RGB or RGBA in sRGB gamma-space,
/// \python As either 0-1 floats or 0-255 integers, with separate alpha.
/// \py The colors are interpreted as RGB or RGBA in sRGB gamma-space,
/// \py As either 0-1 floats or 0-255 integers, with separate alpha.
colors: [rerun.components.Color] ("attr.rerun.component_recommended", nullable, order: 2100);

// --- Optional ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ union TensorBuffer (
/// First comes entire image in Y, followed by interleaved lines ordered as U0, V0, U1, V1, etc.
NV12: NV12Buffer (transparent),

/// YUY2, also known as YUYV is a YUV 4:2:2 chrome downsampled format with 8 bits per channel.
/// YUY2, also known as YUYV is a YUV 4:2:2 chroma downsampled format with 8 bits per channel.
///
/// The order of the channels is Y0, U0, Y1, V0.
YUY2: YUY2Buffer (transparent),
Expand Down
4 changes: 4 additions & 0 deletions crates/re_types/src/archetypes/image.rs

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

2 changes: 1 addition & 1 deletion crates/re_types/src/datatypes/tensor_buffer.rs

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

190 changes: 76 additions & 114 deletions crates/re_types_builder/src/codegen/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,6 @@ fn is_blank<T: AsRef<str>>(line: T) -> bool {
line.as_ref().chars().all(char::is_whitespace)
}

/// Retrieves the global and tagged documentation from a [`Docs`] object.
pub fn get_documentation(docs: &Docs, tags: &[&str]) -> Vec<String> {
let mut lines = docs.doc.clone();

for tag in tags {
lines.extend(
docs.tagged_docs
.get(*tag)
.unwrap_or(&Vec::new())
.iter()
.cloned(),
);
}

// NOTE: remove duplicated blank lines.
lines.dedup();

// NOTE: remove trailing blank lines.
while let Some(line) = lines.last() {
if line.is_empty() {
lines.pop();
} else {
break;
}
}

lines
}

#[derive(Clone)]
pub struct ExampleInfo<'a> {
/// The snake_case name of the example.
Expand All @@ -60,66 +31,62 @@ pub struct ExampleInfo<'a> {

impl<'a> ExampleInfo<'a> {
/// Parses e.g. `// \example example_name title="Example Title" image="https://www.example.com/img.png"`
pub fn parse(tag_content: &'a impl AsRef<str>) -> Self {
fn mono(tag_content: &str) -> ExampleInfo<'_> {
fn find_keyed<'a>(tag: &str, args: &'a str) -> Option<&'a str> {
let mut prev_end = 0;
loop {
if prev_end + tag.len() + "=\"\"".len() >= args.len() {
return None;
}
let key_start = prev_end + args[prev_end..].find(tag)?;
let key_end = key_start + tag.len();
if !args[key_end..].starts_with("=\"") {
prev_end = key_end;
continue;
};
let value_start = key_end + "=\"".len();
let Some(mut value_end) = args[value_start..].find('"') else {
prev_end = value_start;
continue;
};
value_end += value_start;
return Some(&args[value_start..value_end]);
pub fn parse(tag_content: &'a str) -> Self {
fn find_keyed<'a>(tag: &str, args: &'a str) -> Option<&'a str> {
let mut prev_end = 0;
loop {
if prev_end + tag.len() + "=\"\"".len() >= args.len() {
return None;
}
let key_start = prev_end + args[prev_end..].find(tag)?;
let key_end = key_start + tag.len();
if !args[key_end..].starts_with("=\"") {
prev_end = key_end;
continue;
};
let value_start = key_end + "=\"".len();
let Some(mut value_end) = args[value_start..].find('"') else {
prev_end = value_start;
continue;
};
value_end += value_start;
return Some(&args[value_start..value_end]);
}
}

let tag_content = tag_content.trim();
let (name, args) = tag_content
.split_once(' ')
.map_or((tag_content, None), |(a, b)| (a, Some(b)));

let (mut title, mut image, mut exclude_from_api_docs) = (None, None, false);
let tag_content = tag_content.trim();
let (name, args) = tag_content
.split_once(' ')
.map_or((tag_content, None), |(a, b)| (a, Some(b)));

if let Some(args) = args {
let args = args.trim();
let (mut title, mut image, mut exclude_from_api_docs) = (None, None, false);

exclude_from_api_docs = args.contains("!api");
let args = if let Some(args_without_api_prefix) = args.strip_prefix("!api") {
args_without_api_prefix.trim()
} else {
args
};
if let Some(args) = args {
let args = args.trim();

if args.starts_with('"') {
// \example example_name "Example Title"
title = args.strip_prefix('"').and_then(|v| v.strip_suffix('"'));
} else {
// \example example_name title="Example Title" image="https://static.rerun.io/annotation_context_rects/9b446c36011ed30fce7dc6ed03d5fd9557460f70/1200w.png"
title = find_keyed("title", args);
image = find_keyed("image", args).map(ImageUrl::parse);
}
}
exclude_from_api_docs = args.contains("!api");
let args = if let Some(args_without_api_prefix) = args.strip_prefix("!api") {
args_without_api_prefix.trim()
} else {
args
};

ExampleInfo {
name,
title,
image,
exclude_from_api_docs,
if args.starts_with('"') {
// \example example_name "Example Title"
title = args.strip_prefix('"').and_then(|v| v.strip_suffix('"'));
} else {
// \example example_name title="Example Title" image="https://static.rerun.io/annotation_context_rects/9b446c36011ed30fce7dc6ed03d5fd9557460f70/1200w.png"
title = find_keyed("title", args);
image = find_keyed("image", args).map(ImageUrl::parse);
}
}

mono(tag_content.as_ref())
ExampleInfo {
name,
title,
image,
exclude_from_api_docs,
}
}
}

Expand Down Expand Up @@ -314,46 +281,41 @@ pub fn collect_snippets_for_api_docs<'a>(
extension: &str,
required: bool,
) -> anyhow::Result<Vec<Example<'a>>> {
let mut out = Vec::new();
let base_path = crate::rerun_workspace_path().join("docs/snippets/all");

if let Some(examples) = docs.tagged_docs.get("example") {
let base_path = crate::rerun_workspace_path().join("docs/snippets/all");
let examples: Vec<&'a str> = docs.doc_lines_tagged("example");

for base @ ExampleInfo {
name,
exclude_from_api_docs,
..
} in examples.iter().map(ExampleInfo::parse)
{
if exclude_from_api_docs {
continue;
}
let mut out: Vec<Example<'a>> = Vec::new();

let path = base_path.join(format!("{name}.{extension}"));
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(_) if !required => continue,
Err(err) => {
return Err(err).with_context(|| format!("couldn't open snippet {path:?}"))
}
};
let mut content = content
.split('\n')
.map(String::from)
.skip_while(|line| line.starts_with("//") || line.starts_with(r#"""""#)) // Skip leading comments.
.skip_while(|line| line.trim().is_empty()) // Strip leading empty lines.
.collect_vec();

// trim trailing blank lines
while content.last().is_some_and(is_blank) {
content.pop();
}
for example in &examples {
let base: ExampleInfo<'a> = ExampleInfo::parse(example);
let name = &base.name;
if base.exclude_from_api_docs {
continue;
}

out.push(Example {
base,
lines: content,
});
let path = base_path.join(format!("{name}.{extension}"));
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(_) if !required => continue,
Err(err) => return Err(err).with_context(|| format!("couldn't open snippet {path:?}")),
};
let mut content = content
.split('\n')
.map(String::from)
.skip_while(|line| line.starts_with("//") || line.starts_with(r#"""""#)) // Skip leading comments.
.skip_while(|line| line.trim().is_empty()) // Strip leading empty lines.
.collect_vec();

// trim trailing blank lines
while content.last().is_some_and(is_blank) {
content.pop();
}

out.push(Example {
base,
lines: content,
});
}

Ok(out)
Expand Down
2 changes: 1 addition & 1 deletion crates/re_types_builder/src/codegen/cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,7 @@ fn quote_field_docs(field: &ObjectField) -> TokenStream {
}

fn lines_from_docs(docs: &Docs) -> Vec<String> {
let mut lines = crate::codegen::get_documentation(docs, &["cpp", "c++"]);
let mut lines = docs.doc_lines_for_untagged_and("cpp");

let required = true;
let examples = collect_snippets_for_api_docs(docs, "cpp", required).unwrap_or_default();
Expand Down
13 changes: 4 additions & 9 deletions crates/re_types_builder/src/codegen/docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ use crate::{

type ObjectMap = std::collections::BTreeMap<String, Object>;

use super::common::get_documentation;

macro_rules! putln {
($o:ident) => ( writeln!($o).ok() );
($o:ident, $($tt:tt)*) => ( writeln!($o, $($tt)*).ok() );
Expand Down Expand Up @@ -143,19 +141,16 @@ fn index_page(kind: ObjectKind, order: u64, prelude: &str, objects: &[&Object])
fn object_page(reporter: &Reporter, object: &Object, object_map: &ObjectMap) -> String {
let is_unreleased = object.is_attr_set(crate::ATTR_DOCS_UNRELEASED);

let top_level_docs = get_documentation(&object.docs, &[]);
let top_level_docs = object.docs.untagged();

if top_level_docs.is_empty() {
reporter.error(&object.virtpath, &object.fqname, "Undocumented object");
}

let examples = object
.docs
.tagged_docs
.get("example")
let examples = &object.docs.doc_lines_tagged("example");
let examples = examples
.iter()
.flat_map(|v| v.iter())
.map(ExampleInfo::parse)
.map(|line| ExampleInfo::parse(line))
.collect::<Vec<_>>();

let mut page = String::new();
Expand Down
2 changes: 1 addition & 1 deletion crates/re_types_builder/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(crate) use macros::autogen_warning; // Hack for declaring macros as `pub(cra
// ---

pub(crate) mod common;
use self::common::{get_documentation, StringExt};
use self::common::StringExt;

mod cpp;
mod docs;
Expand Down
11 changes: 5 additions & 6 deletions crates/re_types_builder/src/codegen/python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ fn quote_obj_docs(obj: &Object) -> String {
}

fn lines_from_docs(docs: &Docs) -> Vec<String> {
let mut lines = crate::codegen::get_documentation(docs, &["py", "python"]);
let mut lines = docs.doc_lines_for_untagged_and("py");

let examples = collect_snippets_for_api_docs(docs, "py", true).unwrap();
if !examples.is_empty() {
Expand Down Expand Up @@ -1208,7 +1208,7 @@ fn quote_doc_from_fields(objects: &Objects, fields: &Vec<ObjectField>) -> String
let mut lines = vec!["Must be one of:".to_owned(), String::new()];

for field in fields {
let mut content = crate::codegen::get_documentation(&field.docs, &["py", "python"]);
let mut content = field.docs.doc_lines_for_untagged_and("py");
for line in &mut content {
if line.starts_with(char::is_whitespace) {
line.remove(0);
Expand Down Expand Up @@ -1250,7 +1250,7 @@ fn quote_union_kind_from_fields(fields: &Vec<ObjectField>) -> String {
let mut lines = vec!["Possible values:".to_owned(), String::new()];

for field in fields {
let mut content = crate::codegen::get_documentation(&field.docs, &["py", "python"]);
let mut content = field.docs.doc_lines_for_untagged_and("py");
for line in &mut content {
if line.starts_with(char::is_whitespace) {
line.remove(0);
Expand Down Expand Up @@ -1950,7 +1950,8 @@ fn quote_init_method(
obj.fields
.iter()
.filter_map(|field| {
if field.docs.doc.is_empty() {
let doc_content = field.docs.doc_lines_for_untagged_and("py");
if doc_content.is_empty() {
if !field.is_testing() && obj.fields.len() > 1 {
reporter.error(
&field.virtpath,
Expand All @@ -1960,8 +1961,6 @@ fn quote_init_method(
}
None
} else {
let doc_content =
crate::codegen::get_documentation(&field.docs, &["py", "python"]);
Some(format!(
"{}:\n {}",
field.name,
Expand Down
Loading

0 comments on commit e5ebdf6

Please sign in to comment.