Part of gap 2: Effect › Video (De-Interlace, NTSC Colors)

Adds the Video group to the raster effects, after Sharpen:

- De-Interlace…: Eliminate Odd or Even Fields, Create New Fields by
  Duplication or Interpolation. The fields are the rows of the document's
  raster grid (Document Raster Effects Settings › Resolution, carried to
  the filters as PixelSpace::line) counted from the top of the page, so
  the result is the same at any zoom and moves with the page. A line
  taken out copies the line above (the one below at the top), or takes
  the average of the two.
- NTSC Colors (no options, applied at once): colours a composite
  television signal can't carry, with luma plus chroma past 110 % of white
  or luma minus chroma below -20 % in YIQ, have their chroma cut to fit,
  keeping their luma. Saturated yellow, cyan and pure red lose some
  saturation; legal colours stay as they are.

Catalogue rows with parameter docs, dialog dropdowns, menu, translations,
tests (effects, canvas, command sweep junk, menu) and ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-10 16:46:02 +07:00
parent 7fb2c47006
commit c71af3079c
22 changed files with 313 additions and 23 deletions

File diff suppressed because one or more lines are too long

View File

@ -144,6 +144,7 @@ const WARP: &[&str] = &["Effect", "Warp"];
const BLUR: &[&str] = &["Effect", "Blur"];
const SHARPEN: &[&str] = &["Effect", "Sharpen"];
const PIXELATE: &[&str] = &["Effect", "Pixelate"];
const VIDEO: &[&str] = &["Effect", "Video"];
const PATHFINDER: &[&str] = &["Effect", "Pathfinder"];
const PLUGINS: &[&str] = &["Effect", "Plug-ins"];
const ADJUST: &[&str] = &["Effect", "Color Adjustments"];
@ -331,6 +332,20 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{cellSize: pt 3..300 (5)} redraws the object as randomly placed dots of its colours on a white canvas",
json!({"cellSize": 5.0}),
),
r(
"video.deinterlace",
"De-Interlace…",
VIDEO,
"{eliminate: \"odd\"|\"even\" (\"odd\"; which field lines are taken out), create: \"duplication\"|\"interpolation\" (\"duplication\"; a line taken out becomes a copy of the line above, or the average of the lines above and below)} the field lines are the rows of the document's raster grid (Document Raster Effects Settings › Resolution), numbered from 1 at the top of the page",
json!({"eliminate": "odd", "create": "duplication"}),
),
r(
"video.ntscColors",
"NTSC Colors",
VIDEO,
"{} makes colours a television signal can't carry (luma plus chroma past 110 % of white, or luma minus chroma below −20 %) less saturated, keeping their brightness",
json!({}),
),
];
v.extend([
g(

View File

@ -1,6 +1,6 @@
//! Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Pixelate › Color
//! Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, and Stylize › Glowing
//! Edges): filters over premultiplied RGBA8 pixels.
//! Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Stylize › Glowing
//! Edges, and Video › De-Interlace and NTSC Colors): filters over premultiplied RGBA8 pixels.
//!
//! - Every distance is in document units (points) and becomes pixels through the raster's
//! [`PixelSpace::px`], so an effect looks the same at any zoom and at any Document Raster Effects
@ -13,6 +13,7 @@ mod blur;
mod edges;
mod pixelate;
mod sharpen;
mod video;
use serde_json::Value;
use vectorcraft_geom::{Affine, Point, Rect};
@ -22,7 +23,7 @@ use crate::util::{num, text};
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
pub const PIXEL_EFFECTS: [&str; 8] = [
pub const PIXEL_EFFECTS: [&str; 10] = [
"blur.radial",
"blur.smart",
"pixelate.colorHalftone",
@ -31,6 +32,8 @@ pub const PIXEL_EFFECTS: [&str; 8] = [
"pixelate.pointillize",
"sharpen.unsharpMask",
"stylize.glowingEdges",
"video.deinterlace",
"video.ntscColors",
];
/// A Photoshop-style raster effect with its parameters read and clamped to their ranges. Lengths
@ -59,6 +62,11 @@ pub enum PixelFx {
/// Pixelate › Pointillize: random dots about `cell` across of the object's colours on a white
/// canvas.
Pointillize { cell: f64 },
/// Video › De-Interlace: the odd (or `even`) field lines made again from the others, by
/// duplication or, with `interpolate`, by averaging.
DeInterlace { even: bool, interpolate: bool },
/// Video › NTSC Colors: colours a television signal can't carry made less saturated.
NtscColors,
}
/// What a raster's colour channels hold, for the filters that treat them apart (Color Halftone
@ -89,6 +97,9 @@ pub struct PixelSpace {
pub center: Point,
/// What the colour channels hold.
pub channels: Channels,
/// The height of a line of the document's raster grid (document units: 72 / Document Raster
/// Effects Settings › Resolution), the fields De-Interlace works on.
pub line: f64,
}
/// Effect `id`'s parameters `p` (defaults merged in) as a [`PixelFx`]; `None` for other effects.
@ -126,6 +137,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
"pixelate.crystallize" => PixelFx::Crystallize { cell: num(p, "cellSize", 10.0).clamp(3.0, 300.0) },
"pixelate.mezzotint" => PixelFx::Mezzotint { kind: Mezzotint::parse(text(p, "type", "fineDots")) },
"pixelate.pointillize" => PixelFx::Pointillize { cell: num(p, "cellSize", 5.0).clamp(3.0, 300.0) },
"video.deinterlace" => PixelFx::DeInterlace {
even: text(p, "eliminate", "odd").eq_ignore_ascii_case("even"),
interpolate: text(p, "create", "duplication").eq_ignore_ascii_case("interpolation"),
},
"video.ntscColors" => PixelFx::NtscColors,
_ => return None,
})
}
@ -143,7 +159,7 @@ impl PixelFx {
PixelFx::SmartBlur { radius, .. } => radius,
PixelFx::UnsharpMask { .. } => 0.0,
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
PixelFx::ColorHalftone { .. } | PixelFx::Mezzotint { .. } => 0.0,
PixelFx::ColorHalftone { .. } | PixelFx::Mezzotint { .. } | PixelFx::DeInterlace { .. } | PixelFx::NtscColors => 0.0,
// A crystal's or dot's point inside the object reaches out by up to its cell.
PixelFx::Crystallize { cell } | PixelFx::Pointillize { cell } => 1.5 * cell,
}
@ -163,6 +179,9 @@ impl PixelFx {
PixelFx::Mezzotint { .. } => Some(0.0),
// The dots' points, and the softened colour around them.
PixelFx::Pointillize { cell } => Some(2.5 * cell),
// The lines above and below.
PixelFx::DeInterlace { .. } => Some(video::MAX_LINE),
PixelFx::NtscColors => Some(0.0),
}
}
@ -180,6 +199,8 @@ impl PixelFx {
PixelFx::Crystallize { cell } => pixelate::crystallize(px, w, h, space, cell),
PixelFx::Mezzotint { kind } => pixelate::mezzotint(px, w, h, space, kind),
PixelFx::Pointillize { cell } => pixelate::pointillize(px, w, h, space, cell),
PixelFx::DeInterlace { even, interpolate } => video::deinterlace(px, w, h, space, even, interpolate),
PixelFx::NtscColors => video::ntsc(px),
}
}
}

View File

@ -7,7 +7,7 @@ use crate::{RasterFx, raster_effects};
/// A `w` × `h` raster whose pixels are document points (origin top-left).
fn space(w: usize, h: usize) -> PixelSpace {
PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(w as f64 / 2.0, h as f64 / 2.0), channels: Channels::Rgb }
PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(w as f64 / 2.0, h as f64 / 2.0), channels: Channels::Rgb, line: 1.0 }
}
fn image(w: usize, h: usize, f: impl Fn(usize, usize) -> [u8; 4]) -> Vec<u8> {
@ -224,7 +224,8 @@ fn lengths_are_document_units() {
let edge = |scale: usize| image(40 * scale, 4, move |x, _| grey(if x < 20 * scale { 100 } else { 150 }));
let run = |scale: usize| {
let mut d = edge(scale);
let s = PixelSpace { to_doc: Affine::scale(1.0 / scale as f64), px: 1.0 / scale as f64, center: Point::ZERO, channels: Channels::Rgb };
let s =
PixelSpace { to_doc: Affine::scale(1.0 / scale as f64), px: 1.0 / scale as f64, center: Point::ZERO, channels: Channels::Rgb, line: 1.0 };
fx("sharpen.unsharpMask", json!({"amount": 100, "radius": 2})).apply(&mut d, 40 * scale, 4, &s);
(0..40 * scale).filter(|x| at(&d, 40 * scale, *x, 2)[0] < 99).count()
};
@ -253,7 +254,7 @@ fn gaussian_and_plane_blurs_keep_their_mass() {
fn pixel_timings() {
let (w, h) = (2000, 2000);
let src = image(w, h, |x, y| [(x % 256) as u8, (y % 256) as u8, ((x ^ y) % 256) as u8, 255]);
let s = PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(1000.0, 1000.0), channels: Channels::Rgb };
let s = PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(1000.0, 1000.0), channels: Channels::Rgb, line: 1.0 };
let cases = [
("blur.radial", json!({"quality": "draft"})),
("blur.radial", json!({"quality": "good"})),
@ -477,3 +478,50 @@ fn pixelate_patterns_follow_the_object_at_any_resolution() {
assert_eq!(shifted, one, "{id}");
}
}
/// Video › De-Interlace: lines 1, 3, 5… (or 2, 4, 6…) are made again from the others, by copying
/// the line above or by averaging the lines above and below; the lines are the document's raster
/// rows, whatever the raster's own resolution.
#[test]
fn deinterlace_remakes_one_field_from_the_other() {
let (w, h) = (2, 6);
// Line n (from 1) is grey 40 n.
let src = image(w, h, |_, y| grey(40 * (y as u8 + 1)));
let run = |p: serde_json::Value, s: &PixelSpace| {
let mut d = src.clone();
fx("video.deinterlace", p).apply(&mut d, w, h, s);
(0..h).map(|y| at(&d, w, 1, y)[0]).collect::<Vec<_>>()
};
let s = space(w, h);
// Odd lines (1, 3, 5) copy the line above; line 1 has none, so it takes line 2.
assert_eq!(run(json!({}), &s), [80, 80, 80, 160, 160, 240]);
assert_eq!(run(json!({"eliminate": "even"}), &s), [40, 40, 120, 120, 200, 200]);
// Interpolated: the average of the lines around (one side at the edges).
assert_eq!(run(json!({"eliminate": "even", "create": "interpolation"}), &s), [40, 80, 120, 160, 200, 200]);
// Lines two pixels tall: lines 1 and 3 (pixels 0–1 and 4–5) copy their neighbour, row by row.
let coarse = PixelSpace { line: 2.0, ..space(w, h) };
assert_eq!(run(json!({}), &coarse), [120, 160, 120, 160, 120, 160]);
// A damaged space changes nothing.
assert_eq!(run(json!({}), &PixelSpace { line: f64::NAN, ..space(w, h) }), [40, 80, 120, 160, 200, 240]);
}
/// Video › NTSC Colors: saturated yellow and cyan lose saturation (their composite signal is too
/// strong) but keep their brightness; greys, dark colours and transparency stay as they are.
#[test]
fn ntsc_colors_tame_only_colours_too_strong_for_the_signal() {
let (w, h) = (5, 1);
let px = [[255, 255, 0, 255], [0, 255, 255, 255], [128, 128, 128, 255], [100, 20, 20, 255], [0, 0, 0, 0]];
let mut d: Vec<u8> = px.concat();
fx("video.ntscColors", json!({})).apply(&mut d, w, h, &space(w, h));
let luma = |p: [u8; 4]| 0.299 * f64::from(p[0]) + 0.587 * f64::from(p[1]) + 0.114 * f64::from(p[2]);
for (x, &a) in px.iter().enumerate() {
let b = at(&d, w, x, 0);
if x >= 2 {
assert_eq!(b, a, "{x}: unchanged");
continue;
}
let spread = |p: [u8; 4]| p[..3].iter().max().unwrap() - p[..3].iter().min().unwrap();
assert!(spread(b) < spread(a), "{x}: less saturated {b:?}");
assert!((luma(a) - luma(b)).abs() < 3.0, "{x}: as bright {a:?} {b:?}");
}
}

View File

@ -0,0 +1,74 @@
//! Effect › Video: De-Interlace and NTSC Colors.
use vectorcraft_geom::Point;
use super::PixelSpace;
/// The tallest field line De-Interlace reads across (document units): one pixel at the lowest
/// document raster resolution, 1 ppi.
pub(super) const MAX_LINE: f64 = 72.0;
/// De-Interlace: the odd (or, with `even`, the even) fields of the image are made again from the
/// others. The fields are the lines of the document's raster grid (each [`PixelSpace::line`]
/// tall, numbered from 1 at the top of the page): a line taken out becomes a copy of the line
/// above it (the one below at the top), or with `interpolate` the average of the two.
pub(super) fn deinterlace(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, even: bool, interpolate: bool) {
let line = space.line;
let to_px = space.to_doc.inverse();
if !(line.is_finite() && line > 0.0 && to_px.is_finite() && space.to_doc.is_finite()) {
return;
}
let src = px.to_vec();
// The pixel holding document point `q`.
let at = |q: Point| {
let p = to_px * q;
(p.x >= 0.0 && p.y >= 0.0 && p.x < w as f64 && p.y < h as f64).then(|| src.get(p.y as usize * w + p.x as usize).copied()).flatten()
};
for (i, out) in px.iter_mut().enumerate().take(w.saturating_mul(h)) {
let q = space.to_doc * Point::new((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
let k = (q.y / line).floor();
// Line k + 1: odd lines have an even k.
if !k.is_finite() || ((k as i64).rem_euclid(2) == 1) != even {
continue;
}
let (above, below) = (at(Point::new(q.x, q.y - line)), at(Point::new(q.x, q.y + line)));
let made = match (interpolate, above, below) {
(true, Some(a), Some(b)) => Some([0, 1, 2, 3].map(|c| (u16::from(a[c]) + u16::from(b[c])).div_ceil(2) as u8)),
(_, a, b) => a.or(b),
};
if let Some(v) = made {
*out = v;
}
}
}
/// NTSC Colors: colours a television's composite signal can't carry are made less saturated,
/// keeping their brightness. In YIQ, the luma plus the chroma's amplitude must stay within 110 %
/// of white and the luma minus it above −20 %; a colour past either has its chroma cut to fit.
pub(super) fn ntsc(px: &mut [[u8; 4]]) {
for p in px.iter_mut() {
let a = f32::from(p[3]) / 255.0;
if a <= 0.0 {
continue;
}
// Straight colour (the pixels are premultiplied).
let [r, g, b] = [p[0], p[1], p[2]].map(|v| (f32::from(v) / 255.0 / a).min(1.0));
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let (i, q) = (0.596 * r - 0.274 * g - 0.322 * b, 0.211 * r - 0.523 * g + 0.312 * b);
let c = i.hypot(q);
let mut keep = 1.0f32;
if y + c > 1.1 {
keep = keep.min((1.1 - y) / c);
}
if y - c < -0.2 {
keep = keep.min((y + 0.2) / c);
}
if keep >= 1.0 || !keep.is_finite() {
continue;
}
let (i, q) = (i * keep.max(0.0), q * keep.max(0.0));
let rgb = [y + 0.956 * i + 0.621 * q, y - 0.272 * i - 0.647 * q, y - 1.106 * i + 1.703 * q];
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * a * 255.0).round() as u8);
*p = [r, g, b, p[3]];
}
}

View File

@ -21,6 +21,10 @@ const EFFECTS: [(&str, &str); 3] = [
const PIXELATE: [(&str, &str); 4] =
[("pixelate.colorHalftone", "{}"), ("pixelate.crystallize", "{}"), ("pixelate.mezzotint", "{}"), ("pixelate.pointillize", "{}")];
/// Effect › Video (NTSC Colors leaves the striped art's legal colours as they are: see
/// `ntsc_colors_draws_saturated_yellow_safer`).
const VIDEO: [(&str, &str); 1] = [("video.deinterlace", r#"{"create": "interpolation"}"#)];
/// A group of a red square and a darker red stripe across it; returns the group's id.
fn striped() -> (Session, u64) {
let mut s = Session::new();
@ -57,9 +61,11 @@ fn listed_applied_edited_and_undone() {
let menus: Vec<Value> =
EFFECTS.iter().map(|(id, _)| list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == *id).unwrap()["menu"].clone()).collect();
assert_eq!(menus, [json!(["Effect", "Blur"]), json!(["Effect", "Blur"]), json!(["Effect", "Sharpen"])]);
for (id, _) in PIXELATE {
let e = list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == id).unwrap_or_else(|| panic!("{id} listed"));
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", "Pixelate"])), "{id}");
for (group, menu) in [(PIXELATE.as_slice(), "Pixelate"), (VIDEO.as_slice(), "Video"), ([("video.ntscColors", "{}")].as_slice(), "Video")] {
for (id, _) in group {
let e = list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == *id).unwrap_or_else(|| panic!("{id} listed"));
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", menu])), "{id}");
}
}
// Junk parameters are stored as given and read clamped; the canvas still draws.
for (id, _) in EFFECTS {
@ -80,7 +86,7 @@ fn listed_applied_edited_and_undone() {
#[test]
fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
for (id, params) in EFFECTS.into_iter().chain(PIXELATE) {
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(VIDEO) {
let (mut s, g) = striped();
let plain = render(&s);
let params: Value = serde_json::from_str(params).unwrap();
@ -109,7 +115,7 @@ fn spin_blur_reaches_past_the_corners() {
#[test]
fn pdf_and_svg_write_the_effected_object_as_an_image() {
for (id, params) in EFFECTS.into_iter().chain(PIXELATE) {
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(VIDEO) {
let (mut s, g) = striped();
let params: Value = serde_json::from_str(params).unwrap();
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": params})).unwrap();
@ -140,3 +146,22 @@ fn expand_appearance_makes_an_image() {
s.execute("effect.expandAppearance", &json!({"ids": [g]})).unwrap();
assert!(matches!(s.doc().unwrap().doc.node(NodeId(g)).unwrap().kind, NodeKind::Image(_)));
}
/// Video › NTSC Colors on the canvas: a saturated yellow, too strong for a television signal, draws
/// less saturated; the effect is listed and applies without options.
#[test]
fn ntsc_colors_draws_saturated_yellow_safer() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let id = s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
s.execute("paint.setFill", &json!({"color": "#ffff00", "ids": [id]})).unwrap();
s.execute("paint.setStroke", &json!({"none": true, "ids": [id]})).unwrap();
let before = render(&s);
s.execute("effect.apply", &json!({"effect": "video.ntscColors", "ids": [id]})).unwrap();
let after = render(&s);
let px = |r: &Rendered| r.pixel(100, 100);
let (a, b) = (px(&before), px(&after));
assert_eq!(&a[..3], &[255, 255, 0]);
assert!(b[2] > 20 && b[0] < 255 && b[3] == 255, "less saturated: {b:?}");
assert_eq!(after.pixels, render(&s).pixels, "deterministic");
}

View File

@ -282,6 +282,8 @@ fn structured_junk() {
("effect.apply", json!({"effect": "pixelate.crystallize", "params": {"cellSize": -1e308}})),
("effect.apply", json!({"effect": "pixelate.mezzotint", "params": {"type": 5}})),
("effect.apply", json!({"effect": "pixelate.pointillize", "params": {"cellSize": "NaN"}})),
("effect.apply", json!({"effect": "video.deinterlace", "params": {"eliminate": 7, "create": ["x"]}})),
("effect.apply", json!({"effect": "video.ntscColors", "params": {"junk": 1e308}})),
("effect.apply", json!({"effect": "no.such.effect"})),
("effect.remove", json!({"index": 99})),
("effect.setParams", json!({"index": 0, "params": null})),

View File

@ -601,7 +601,9 @@ impl Renderer {
Ink::Display if f.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk => effects::pixel::Channels::Cmyk,
Ink::Display => effects::pixel::Channels::Rgb,
};
let space = effects::PixelSpace { to_doc, px, center: content.reach.center(), channels };
// De-Interlace's field lines: the rows of the document's raster grid (1 to 2400 ppi).
let ppi = Some(f.doc.raster_effects_ppi).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0);
let space = effects::PixelSpace { to_doc, px, center: content.reach.center(), channels, line: 72.0 / ppi };
let (wz, hz) = (wu as usize, hu as usize);
for fx in chain {
match fx {

View File

@ -160,6 +160,8 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
("blur.radial", "quality") => Some(&[("Draft", "draft"), ("Good", "good"), ("Best", "best")]),
("blur.smart", "quality") => Some(&[("Low", "low"), ("Medium", "medium"), ("High", "high")]),
("pixelate.mezzotint", "type") => Some(&vectorcraft_effects::pixel::MEZZOTINT_TYPES),
("video.deinterlace", "eliminate") => Some(&[("Odd Fields", "odd"), ("Even Fields", "even")]),
("video.deinterlace", "create") => Some(&[("Duplication", "duplication"), ("Interpolation", "interpolation")]),
("path.offsetPath", "joins") => Some(&[("Miter", "miter"), ("Round", "round"), ("Bevel", "bevel")]),
("distort.roughen" | "distort.zigZag", "points") => Some(&[("Smooth", "smooth"), ("Corner", "corner")]),
_ => None,

View File

@ -357,6 +357,7 @@ pub(super) fn humanize(k: &str) -> String {
"Channel4" => "Channel 4:".into(),
"Include Cmy Blacks" => "Include Blacks with CMY:".into(),
"Align To Path" => "Align to Path:".into(),
"Create" => "Create New Fields by:".into(),
_ => format!("{s}:"),
}
}

View File

@ -657,3 +657,6 @@
Crystallize… Krystalizace…
Mezzotint… Mezzotinta…
Pointillize… Pointilismus…
Video Video
De-Interlace… Odstranit prokládání…
NTSC Colors Barvy NTSC

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3871,3 +3871,13 @@ axis Both Ambos
@msg the PSD file is cut short el archivo PSD está incompleto
@msg the PSD file's header is damaged el encabezado del archivo PSD está dañado
@msg the PSD file's image data can't be read: {e} no se pueden leer los datos de imagen del archivo PSD: {e}
Video Video
De-Interlace… Desentrelazar…
De-Interlace Desentrelazar
NTSC Colors Colores NTSC
Eliminate Eliminar
Create New Fields by Crear campos nuevos por
Odd Fields Campos impares
Even Fields Campos pares
Duplication Duplicación
Interpolation Interpolación

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3882,3 +3882,13 @@ axis Both Les deux
@msg the PSD file is cut short le fichier PSD est tronqué
@msg the PSD file's header is damaged l'en-tête du fichier PSD est endommagé
@msg the PSD file's image data can't be read: {e} les données d'image du fichier PSD ne peuvent pas être lues : {e}
Video Vidéo
De-Interlace… Désentrelacement…
De-Interlace Désentrelacement
NTSC Colors Couleurs NTSC
Eliminate Supprimer
Create New Fields by Créer des trames par
Odd Fields Trames impaires
Even Fields Trames paires
Duplication Duplication
Interpolation Interpolation

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3871,3 +3871,13 @@ axis Both Entrambi
@msg the PSD file is cut short il file PSD è troncato
@msg the PSD file's header is damaged l'intestazione del file PSD è danneggiata
@msg the PSD file's image data can't be read: {e} i dati immagine del file PSD non possono essere letti: {e}
Video Video
De-Interlace… Deinterlaccia…
De-Interlace Deinterlaccia
NTSC Colors Colori NTSC
Eliminate Elimina
Create New Fields by Crea nuovi campi per
Odd Fields Campi dispari
Even Fields Campi pari
Duplication Duplicazione
Interpolation Interpolazione

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3863,3 +3863,13 @@ theme Appearance 外観
@msg the PSD file is cut short PSD ファイルが途中で切れています
@msg the PSD file's header is damaged PSD ファイルのヘッダーが破損しています
@msg the PSD file's image data can't be read: {e} PSD ファイルの画像データを読み込めません:{e}
Video ビデオ
De-Interlace… インターレース解除…
De-Interlace インターレース解除
NTSC Colors NTSC カラー
Eliminate 削除
Create New Fields by 新規フィールドの作成
Odd Fields 奇数フィールド
Even Fields 偶数フィールド
Duplication 複製
Interpolation 補間

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2718,3 +2718,6 @@
Crystallize… Cristalizar…
Mezzotint… Meia-Tinta…
Pointillize… Pontilhar…
Video Vídeo
De-Interlace… Desentrelaçar…
NTSC Colors Cores NTSC

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3869,3 +3869,13 @@ axis Both Оба
@msg the PSD file is cut short файл PSD обрезан
@msg the PSD file's header is damaged заголовок файла PSD повреждён
@msg the PSD file's image data can't be read: {e} не удаётся прочитать данные изображения файла PSD: {e}
Video Видео
De-Interlace… Устранение чересстрочности…
De-Interlace Устранение чересстрочности
NTSC Colors Цвета NTSC
Eliminate Удалить
Create New Fields by Создать новые поля
Odd Fields Нечётные поля
Even Fields Чётные поля
Duplication Дублированием
Interpolation Интерполяцией

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3854,3 +3854,13 @@ axis Both Обидві
@msg the PSD file is cut short файл PSD обрізаний
@msg the PSD file's header is damaged заголовок файлу PSD пошкоджено
@msg the PSD file's image data can't be read: {e} не вдається прочитати дані зображення файлу PSD: {e}
Video Відео
De-Interlace… Усунення черезрядковості…
De-Interlace Усунення черезрядковості
NTSC Colors Кольори NTSC
Eliminate Вилучити
Create New Fields by Створити нові поля
Odd Fields Непарні поля
Even Fields Парні поля
Duplication Дублюванням
Interpolation Інтерполяцією

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -3504,3 +3504,13 @@ axis Both 两者
Draft 草稿
Good 良好
Best 最佳
Video 视频
De-Interlace… 逐行…
De-Interlace 逐行
NTSC Colors NTSC 颜色
Eliminate 消除
Create New Fields by 创建新场方式
Odd Fields 奇数场
Even Fields 偶数场
Duplication 复制
Interpolation 插值

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3282,3 +3282,13 @@ axis Both 兩者
Draft 草稿
Good 良好
Best 最佳
Video 視訊
De-Interlace… 反交錯…
De-Interlace 反交錯
NTSC Colors NTSC 色彩
Eliminate 消除
Create New Fields by 建立新圖場以
Odd Fields 奇數圖場
Even Fields 偶數圖場
Duplication 複製
Interpolation 內插

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3305,8 +3305,8 @@ fn font_items(english_names: bool) -> Vec<Item> {
/// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the
/// vector effects): (submenu, the catalogue's menu path of its effects).
const RASTER_MENUS: [(&str, &[&str]); 3] =
[("Blur", &["Effect", "Blur"]), ("Pixelate", &["Effect", "Pixelate"]), ("Sharpen", &["Effect", "Sharpen"])];
const RASTER_MENUS: [(&str, &[&str]); 4] =
[("Blur", &["Effect", "Blur"]), ("Pixelate", &["Effect", "Pixelate"]), ("Sharpen", &["Effect", "Sharpen"]), ("Video", &["Effect", "Video"])];
/// The Effect menu, built from the effects catalogue (vector effects), plus raster effects.
fn effect_menu() -> Vec<Item> {

View File

@ -64,6 +64,20 @@ fn effect_menu_lists_the_pixelate_filters_between_blur_and_sharpen() {
assert!(at("Blur") < at("Pixelate") && at("Pixelate") < at("Sharpen"));
}
/// Effect › Video, after Sharpen: De-Interlace opens its dialog, NTSC Colors (no options) applies.
#[test]
fn effect_menu_lists_the_video_filters_last() {
let app = app();
let entries = menus::menu_entries(&app);
let video: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Video"]).collect();
let items: Vec<(&str, Option<&str>)> = video.iter().map(|e| (e.label.as_str(), e.command.as_deref())).collect();
assert_eq!(items, [("De-Interlace…", Some("effect.dialog")), ("NTSC Colors", Some("effect.apply"))]);
let raster: Vec<&str> =
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
assert!(at("Sharpen") < at("Video"));
}
#[test]
fn library_submenus_are_disabled_placeholders() {
let app = app();