Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/text
diff options
context:
space:
mode:
authorMathieu Duponchelle <mathieu@centricular.com>2020-04-13 20:39:52 +0300
committerMathieu Duponchelle <mathieu@centricular.com>2020-04-16 21:46:21 +0300
commit7701850586d1f0004afd6c3b40c2452171f88cc1 (patch)
tree13a91661c22871268167f1fddb2c8018766e00ea /text
parent92163c46b23b6253fa22f36816b29479c07eee4c (diff)
Implement text wrapping element around the textwrap crate
Diffstat (limited to 'text')
-rw-r--r--text/wrap/Cargo.toml31
-rw-r--r--text/wrap/build.rs5
-rw-r--r--text/wrap/src/gsttextwrap.rs423
-rw-r--r--text/wrap/src/lib.rs43
-rw-r--r--text/wrap/tests/textwrap.rs123
5 files changed, 625 insertions, 0 deletions
diff --git a/text/wrap/Cargo.toml b/text/wrap/Cargo.toml
new file mode 100644
index 000000000..7b25b6112
--- /dev/null
+++ b/text/wrap/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "gst-plugin-textwrap"
+version = "0.6.0"
+authors = ["Mathieu Duponchelle <mathieu@centricular.com>"]
+license = "LGPL-2.1-or-later"
+edition = "2018"
+description = "Rust Text Wrap Plugin"
+repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
+
+[dependencies]
+glib = { git = "https://github.com/gtk-rs/glib" }
+once_cell = "1.0"
+textwrap = { version = "0.11", features = ["hyphenation"] }
+hyphenation = "0.7.1"
+
+[dependencies.gst]
+git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
+features = ["v1_12"]
+package="gstreamer"
+
+[lib]
+name = "gstrstextwrap"
+crate-type = ["cdylib", "rlib"]
+path = "src/lib.rs"
+
+[build-dependencies]
+gst-plugin-version-helper = { path="../../version-helper" }
+
+[dev-dependencies.gst-check]
+git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
+package="gstreamer-check"
diff --git a/text/wrap/build.rs b/text/wrap/build.rs
new file mode 100644
index 000000000..fe307a434
--- /dev/null
+++ b/text/wrap/build.rs
@@ -0,0 +1,5 @@
+use gst_plugin_version_helper;
+
+fn main() {
+ gst_plugin_version_helper::get_info()
+}
diff --git a/text/wrap/src/gsttextwrap.rs b/text/wrap/src/gsttextwrap.rs
new file mode 100644
index 000000000..7644259d5
--- /dev/null
+++ b/text/wrap/src/gsttextwrap.rs
@@ -0,0 +1,423 @@
+// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Library General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Library General Public License for more details.
+//
+// You should have received a copy of the GNU Library General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 51 Franklin Street, Suite 500,
+// Boston, MA 02110-1335, USA.
+
+use glib;
+use glib::prelude::*;
+use glib::subclass;
+use glib::subclass::prelude::*;
+use gst;
+use gst::prelude::*;
+use gst::subclass::prelude::*;
+
+use std::default::Default;
+use std::fs::File;
+use std::io;
+use std::sync::Mutex;
+
+use once_cell::sync::Lazy;
+
+use hyphenation::{Load, Standard};
+use textwrap;
+
+static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "textwrap",
+ gst::DebugColorFlags::empty(),
+ Some("Text wrapper element"),
+ )
+});
+
+const DEFAULT_DICTIONARY: Option<String> = None;
+const DEFAULT_COLUMNS: u32 = 32; /* CEA 608 max columns */
+const DEFAULT_LINES: u32 = 0;
+
+static PROPERTIES: [subclass::Property; 3] = [
+ subclass::Property("dictionary", |name| {
+ glib::ParamSpec::string(
+ name,
+ "Dictionary",
+ "Path to a dictionary to load at runtime to perform hyphenation, see \
+ <https://docs.rs/crate/hyphenation/0.7.1> for more information",
+ None,
+ glib::ParamFlags::READWRITE,
+ )
+ }),
+ subclass::Property("columns", |name| {
+ glib::ParamSpec::uint(
+ name,
+ "Columns",
+ "Maximum number of columns for any given line",
+ 1,
+ std::u32::MAX,
+ DEFAULT_COLUMNS,
+ glib::ParamFlags::READWRITE,
+ )
+ }),
+ subclass::Property("lines", |name| {
+ glib::ParamSpec::uint(
+ name,
+ "Lines",
+ "Split input buffer into output buffers with max lines (0=do not split)",
+ 0,
+ std::u32::MAX,
+ DEFAULT_LINES,
+ glib::ParamFlags::READWRITE,
+ )
+ }),
+];
+
+#[derive(Debug, Clone)]
+struct Settings {
+ dictionary: Option<String>,
+ columns: u32,
+ lines: u32,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ dictionary: DEFAULT_DICTIONARY,
+ columns: DEFAULT_COLUMNS, /* CEA 608 max columns */
+ lines: DEFAULT_LINES,
+ }
+ }
+}
+
+#[allow(clippy::large_enum_variant)]
+enum Wrapper {
+ H(textwrap::Wrapper<'static, Standard>),
+ N(textwrap::Wrapper<'static, textwrap::NoHyphenation>),
+}
+
+struct State {
+ wrapper: Option<Wrapper>,
+}
+
+impl Wrapper {
+ fn fill(&self, s: &str) -> String {
+ match *self {
+ Wrapper::H(ref w) => w.fill(s),
+ Wrapper::N(ref w) => w.fill(s),
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self { wrapper: None }
+ }
+}
+
+struct TextWrap {
+ srcpad: gst::Pad,
+ sinkpad: gst::Pad,
+ settings: Mutex<Settings>,
+ state: Mutex<State>,
+}
+
+impl TextWrap {
+ fn set_pad_functions(sinkpad: &gst::Pad) {
+ sinkpad.set_chain_function(|pad, parent, buffer| {
+ TextWrap::catch_panic_pad_function(
+ parent,
+ || Err(gst::FlowError::Error),
+ |textwrap, element| textwrap.sink_chain(pad, element, buffer),
+ )
+ });
+ }
+
+ fn update_wrapper(&self, element: &gst::Element) {
+ let settings = self.settings.lock().unwrap();
+ let mut state = self.state.lock().unwrap();
+
+ if state.wrapper.is_some() {
+ return;
+ }
+
+ state.wrapper = if let Some(dictionary) = &settings.dictionary {
+ let dict_file = match File::open(dictionary) {
+ Err(err) => {
+ gst_error!(CAT, obj: element, "Failed to open dictionary file: {}", err);
+ return;
+ }
+ Ok(dict_file) => dict_file,
+ };
+
+ let mut reader = io::BufReader::new(dict_file);
+ let standard = match Standard::any_from_reader(&mut reader) {
+ Err(err) => {
+ gst_error!(
+ CAT,
+ obj: element,
+ "Failed to load standard from file: {}",
+ err
+ );
+ return;
+ }
+ Ok(standard) => standard,
+ };
+
+ Some(Wrapper::H(textwrap::Wrapper::with_splitter(
+ settings.columns as usize,
+ standard,
+ )))
+ } else {
+ Some(Wrapper::N(textwrap::Wrapper::with_splitter(
+ settings.columns as usize,
+ textwrap::NoHyphenation,
+ )))
+ };
+ }
+
+ fn sink_chain(
+ &self,
+ _pad: &gst::Pad,
+ element: &gst::Element,
+ buffer: gst::Buffer,
+ ) -> Result<gst::FlowSuccess, gst::FlowError> {
+ self.update_wrapper(element);
+
+ let mut pts: gst::ClockTime = buffer
+ .get_pts()
+ .ok_or_else(|| {
+ gst_error!(CAT, obj: element, "Need timestamped buffers");
+ gst::FlowError::Error
+ })?
+ .into();
+
+ let duration: gst::ClockTime = buffer
+ .get_duration()
+ .ok_or_else(|| {
+ gst_error!(CAT, obj: element, "Need buffers with duration");
+ gst::FlowError::Error
+ })?
+ .into();
+
+ let data = buffer.map_readable().map_err(|_| {
+ gst_error!(CAT, obj: element, "Can't map buffer readable");
+
+ gst::FlowError::Error
+ })?;
+
+ let data = std::str::from_utf8(&data).map_err(|err| {
+ gst_error!(CAT, obj: element, "Can't decode utf8: {}", err);
+
+ gst::FlowError::Error
+ })?;
+
+ let lines = self.settings.lock().unwrap().lines;
+
+ let data = {
+ let state = self.state.lock().unwrap();
+ let wrapper = state
+ .wrapper
+ .as_ref()
+ .expect("We should have a wrapper by now");
+ wrapper.fill(data)
+ };
+
+ // If the lines property was set, we want to split the result into buffers
+ // of at most N lines. We compute the duration for each of those based on
+ // the total number of words, and the number of words in each of the split-up
+ // buffers.
+ if lines > 0 {
+ let mut bufferlist = gst::BufferList::new();
+ let duration_per_word: gst::ClockTime =
+ duration / data.split_whitespace().count() as u64;
+
+ for chunk in data.lines().collect::<Vec<&str>>().chunks(lines as usize) {
+ let data = chunk.join("\n");
+ let duration: gst::ClockTime =
+ duration_per_word * data.split_whitespace().count() as u64;
+ let mut buf = gst::Buffer::from_mut_slice(data.into_bytes());
+
+ {
+ let buf = buf.get_mut().unwrap();
+
+ buf.set_pts(pts);
+ buf.set_duration(duration);
+ pts += duration;
+ }
+
+ bufferlist.get_mut().unwrap().add(buf);
+ }
+
+ self.srcpad.push_list(bufferlist)
+ } else {
+ let mut buf = gst::Buffer::from_mut_slice(data.into_bytes());
+
+ {
+ let buf = buf.get_mut().unwrap();
+
+ buf.set_pts(pts);
+ buf.set_duration(duration);
+ }
+
+ self.srcpad.push(buf)
+ }
+ }
+}
+
+impl ObjectSubclass for TextWrap {
+ const NAME: &'static str = "RsTextWrap";
+ type ParentType = gst::Element;
+ type Instance = gst::subclass::ElementInstanceStruct<Self>;
+ type Class = subclass::simple::ClassStruct<Self>;
+
+ glib_object_subclass!();
+
+ fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self {
+ let templ = klass.get_pad_template("sink").unwrap();
+ let sinkpad = gst::Pad::new_from_template(&templ, Some("sink"));
+ let templ = klass.get_pad_template("src").unwrap();
+ let srcpad = gst::Pad::new_from_template(&templ, Some("src"));
+
+ srcpad.use_fixed_caps();
+ sinkpad.use_fixed_caps();
+
+ TextWrap::set_pad_functions(&sinkpad);
+
+ let settings = Mutex::new(Settings::default());
+ let state = Mutex::new(State::default());
+
+ Self {
+ srcpad,
+ sinkpad,
+ settings,
+ state,
+ }
+ }
+
+ fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) {
+ klass.set_metadata(
+ "Text Wrapper",
+ "Text/Filter",
+ "Breaks text into fixed-size lines, with optional hyphenationz",
+ "Mathieu Duponchelle <mathieu@centricular.com>",
+ );
+
+ let caps = gst::Caps::builder("text/x-raw")
+ .field("format", &"utf8")
+ .build();
+ let src_pad_template = gst::PadTemplate::new(
+ "src",
+ gst::PadDirection::Src,
+ gst::PadPresence::Always,
+ &caps,
+ )
+ .unwrap();
+ klass.add_pad_template(src_pad_template);
+
+ let sink_pad_template = gst::PadTemplate::new(
+ "sink",
+ gst::PadDirection::Sink,
+ gst::PadPresence::Always,
+ &caps,
+ )
+ .unwrap();
+ klass.add_pad_template(sink_pad_template);
+
+ klass.install_properties(&PROPERTIES);
+ }
+}
+
+impl ObjectImpl for TextWrap {
+ glib_object_impl!();
+
+ fn constructed(&self, obj: &glib::Object) {
+ self.parent_constructed(obj);
+
+ let element = obj.downcast_ref::<gst::Element>().unwrap();
+ element.add_pad(&self.sinkpad).unwrap();
+ element.add_pad(&self.srcpad).unwrap();
+ }
+
+ fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
+ let prop = &PROPERTIES[id];
+
+ match *prop {
+ subclass::Property("dictionary", ..) => {
+ let mut settings = self.settings.lock().unwrap();
+ let mut state = self.state.lock().unwrap();
+ settings.dictionary = value.get().expect("type checked upstream");
+ state.wrapper = None;
+ }
+ subclass::Property("columns", ..) => {
+ let mut settings = self.settings.lock().unwrap();
+ let mut state = self.state.lock().unwrap();
+ settings.columns = value.get_some().expect("type checked upstream");
+ state.wrapper = None;
+ }
+ subclass::Property("lines", ..) => {
+ let mut settings = self.settings.lock().unwrap();
+ settings.lines = value.get_some().expect("type checked upstream");
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
+ let prop = &PROPERTIES[id];
+
+ match *prop {
+ subclass::Property("dictionary", ..) => {
+ let settings = self.settings.lock().unwrap();
+ Ok(settings.dictionary.to_value())
+ }
+ subclass::Property("columns", ..) => {
+ let settings = self.settings.lock().unwrap();
+ Ok(settings.columns.to_value())
+ }
+ subclass::Property("lines", ..) => {
+ let settings = self.settings.lock().unwrap();
+ Ok(settings.lines.to_value())
+ }
+ _ => unimplemented!(),
+ }
+ }
+}
+
+impl ElementImpl for TextWrap {
+ fn change_state(
+ &self,
+ element: &gst::Element,
+ transition: gst::StateChange,
+ ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
+ gst_info!(CAT, obj: element, "Changing state {:?}", transition);
+
+ match transition {
+ gst::StateChange::PausedToReady => {
+ let mut state = self.state.lock().unwrap();
+ *state = State::default();
+ }
+ _ => (),
+ }
+
+ let success = self.parent_change_state(element, transition)?;
+
+ Ok(success)
+ }
+}
+
+pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ gst::Element::register(
+ Some(plugin),
+ "textwrap",
+ gst::Rank::None,
+ TextWrap::get_type(),
+ )
+}
diff --git a/text/wrap/src/lib.rs b/text/wrap/src/lib.rs
new file mode 100644
index 000000000..c8d09eb98
--- /dev/null
+++ b/text/wrap/src/lib.rs
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Library General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Library General Public License for more details.
+//
+// You should have received a copy of the GNU Library General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 51 Franklin Street, Suite 500,
+// Boston, MA 02110-1335, USA.
+
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate glib;
+#[macro_use]
+extern crate gst;
+extern crate hyphenation;
+extern crate textwrap;
+
+mod gsttextwrap;
+
+fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ gsttextwrap::register(plugin)
+}
+
+gst_plugin_define!(
+ rstextwrap,
+ env!("CARGO_PKG_DESCRIPTION"),
+ plugin_init,
+ concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
+ "LGPL",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_REPOSITORY"),
+ env!("BUILD_REL_DATE")
+);
diff --git a/text/wrap/tests/textwrap.rs b/text/wrap/tests/textwrap.rs
new file mode 100644
index 000000000..7c194f857
--- /dev/null
+++ b/text/wrap/tests/textwrap.rs
@@ -0,0 +1,123 @@
+// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Library General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Library General Public License for more details.
+//
+// You should have received a copy of the GNU Library General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 51 Franklin Street, Suite 500,
+// Boston, MA 02110-1335, USA.
+
+use glib::prelude::*;
+
+fn init() {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+
+ INIT.call_once(|| {
+ gst::init().unwrap();
+ gstrstextwrap::plugin_register_static().expect("textwrap test");
+ });
+}
+
+#[test]
+fn test_columns() {
+ init();
+
+ let input = b"Split this text up";
+
+ let expected_output = "Split\nthis\ntext\nup";
+
+ let mut h = gst_check::Harness::new("textwrap");
+
+ {
+ let wrap = h.get_element().expect("Could not create textwrap");
+ wrap.set_property("columns", &5u32).unwrap();
+ }
+
+ h.set_src_caps_str("text/x-raw, format=utf8");
+
+ let buf = {
+ let mut buf = gst::Buffer::from_mut_slice(Vec::from(&input[..]));
+ let buf_ref = buf.get_mut().unwrap();
+ buf_ref.set_pts(gst::ClockTime::from_seconds(0));
+ buf_ref.set_duration(gst::ClockTime::from_seconds(2));
+ buf
+ };
+
+ assert_eq!(h.push(buf), Ok(gst::FlowSuccess::Ok));
+
+ let buf = h.pull().expect("Couldn't pull buffer");
+
+ assert_eq!(buf.get_pts(), 0.into());
+ assert_eq!(buf.get_duration(), 2 * gst::SECOND);
+
+ let map = buf.map_readable().expect("Couldn't map buffer readable");
+
+ assert_eq!(
+ std::str::from_utf8(map.as_ref()),
+ std::str::from_utf8(expected_output.as_ref())
+ );
+}
+
+#[test]
+fn test_lines() {
+ init();
+
+ let input = b"Split this text up";
+
+ let mut h = gst_check::Harness::new("textwrap");
+
+ {
+ let wrap = h.get_element().expect("Could not create textwrap");
+ wrap.set_property("columns", &5u32).unwrap();
+ wrap.set_property("lines", &2u32).unwrap();
+ }
+
+ h.set_src_caps_str("text/x-raw, format=utf8");
+
+ let buf = {
+ let mut buf = gst::Buffer::from_mut_slice(Vec::from(&input[..]));
+ let buf_ref = buf.get_mut().unwrap();
+ buf_ref.set_pts(gst::ClockTime::from_seconds(0));
+ buf_ref.set_duration(gst::ClockTime::from_seconds(2));
+ buf
+ };
+
+ assert_eq!(h.push(buf), Ok(gst::FlowSuccess::Ok));
+
+ let buf = h.pull().expect("Couldn't pull buffer");
+
+ assert_eq!(buf.get_pts(), 0.into());
+ assert_eq!(buf.get_duration(), gst::SECOND);
+
+ let expected_output = "Split\nthis";
+
+ let map = buf.map_readable().expect("Couldn't map buffer readable");
+
+ assert_eq!(
+ std::str::from_utf8(map.as_ref()),
+ std::str::from_utf8(expected_output.as_ref())
+ );
+
+ let buf = h.pull().expect("Couldn't pull buffer");
+
+ assert_eq!(buf.get_pts(), gst::SECOND);
+ assert_eq!(buf.get_duration(), gst::SECOND);
+
+ let expected_output = "text\nup";
+
+ let map = buf.map_readable().expect("Couldn't map buffer readable");
+
+ assert_eq!(
+ std::str::from_utf8(map.as_ref()),
+ std::str::from_utf8(expected_output.as_ref())
+ );
+}