| 1 | //! # Text chunks (tEXt/zTXt/iTXt) structs and functions |
| 2 | //! |
| 3 | //! The [PNG spec](https://www.w3.org/TR/2003/REC-PNG-20031110/#11textinfo) optionally allows for |
| 4 | //! embedded text chunks in the file. They may appear either before or after the image data |
| 5 | //! chunks. There are three kinds of text chunks. |
| 6 | //! - `tEXt`: This has a `keyword` and `text` field, and is ISO 8859-1 encoded. |
| 7 | //! - `zTXt`: This is semantically the same as `tEXt`, i.e. it has the same fields and |
| 8 | //! encoding, but the `text` field is compressed before being written into the PNG file. |
| 9 | //! - `iTXt`: This chunk allows for its `text` field to be any valid UTF-8, and supports |
| 10 | //! compression of the text field as well. |
| 11 | //! |
| 12 | //! The `ISO 8859-1` encoding technically doesn't allow any control characters |
| 13 | //! to be used, but in practice these values are encountered anyway. This can |
| 14 | //! either be the extended `ISO-8859-1` encoding with control characters or the |
| 15 | //! `Windows-1252` encoding. This crate assumes the `ISO-8859-1` encoding is |
| 16 | //! used. |
| 17 | //! |
| 18 | //! ## Reading text chunks |
| 19 | //! |
| 20 | //! As a PNG is decoded, any text chunk encountered is appended the |
| 21 | //! [`Info`](`crate::common::Info`) struct, in the `uncompressed_latin1_text`, |
| 22 | //! `compressed_latin1_text`, and the `utf8_text` fields depending on whether the encountered |
| 23 | //! chunk is `tEXt`, `zTXt`, or `iTXt`. |
| 24 | //! |
| 25 | //! ``` |
| 26 | //! use std::fs::File; |
| 27 | //! use std::iter::FromIterator; |
| 28 | //! use std::path::PathBuf; |
| 29 | //! |
| 30 | //! // Opening a png file that has a zTXt chunk |
| 31 | //! let decoder = png::Decoder::new( |
| 32 | //! File::open(PathBuf::from_iter([ |
| 33 | //! "tests" , |
| 34 | //! "text_chunk_examples" , |
| 35 | //! "ztxt_example.png" , |
| 36 | //! ])) |
| 37 | //! .unwrap(), |
| 38 | //! ); |
| 39 | //! let mut reader = decoder.read_info().unwrap(); |
| 40 | //! // If the text chunk is before the image data frames, `reader.info()` already contains the text. |
| 41 | //! for text_chunk in &reader.info().compressed_latin1_text { |
| 42 | //! println!("{:?}" , text_chunk.keyword); // Prints the keyword |
| 43 | //! println!("{:#?}" , text_chunk); // Prints out the text chunk. |
| 44 | //! // To get the uncompressed text, use the `get_text` method. |
| 45 | //! println!("{}" , text_chunk.get_text().unwrap()); |
| 46 | //! } |
| 47 | //! ``` |
| 48 | //! |
| 49 | //! ## Writing text chunks |
| 50 | //! |
| 51 | //! There are two ways to write text chunks: the first is to add the appropriate text structs directly to the encoder header before the header is written to file. |
| 52 | //! To add a text chunk at any point in the stream, use the `write_text_chunk` method. |
| 53 | //! |
| 54 | //! ``` |
| 55 | //! # use png::text_metadata::{ITXtChunk, ZTXtChunk}; |
| 56 | //! # use std::env; |
| 57 | //! # use std::fs::File; |
| 58 | //! # use std::io::BufWriter; |
| 59 | //! # use std::iter::FromIterator; |
| 60 | //! # use std::path::PathBuf; |
| 61 | //! # let file = File::create(PathBuf::from_iter(["target" , "text_chunk.png" ])).unwrap(); |
| 62 | //! # let ref mut w = BufWriter::new(file); |
| 63 | //! let mut encoder = png::Encoder::new(w, 2, 1); // Width is 2 pixels and height is 1. |
| 64 | //! encoder.set_color(png::ColorType::Rgba); |
| 65 | //! encoder.set_depth(png::BitDepth::Eight); |
| 66 | //! // Adding text chunks to the header |
| 67 | //! encoder |
| 68 | //! .add_text_chunk( |
| 69 | //! "Testing tEXt" .to_string(), |
| 70 | //! "This is a tEXt chunk that will appear before the IDAT chunks." .to_string(), |
| 71 | //! ) |
| 72 | //! .unwrap(); |
| 73 | //! encoder |
| 74 | //! .add_ztxt_chunk( |
| 75 | //! "Testing zTXt" .to_string(), |
| 76 | //! "This is a zTXt chunk that is compressed in the png file." .to_string(), |
| 77 | //! ) |
| 78 | //! .unwrap(); |
| 79 | //! encoder |
| 80 | //! .add_itxt_chunk( |
| 81 | //! "Testing iTXt" .to_string(), |
| 82 | //! "iTXt chunks support all of UTF8. Example: हिंदी." .to_string(), |
| 83 | //! ) |
| 84 | //! .unwrap(); |
| 85 | //! |
| 86 | //! let mut writer = encoder.write_header().unwrap(); |
| 87 | //! |
| 88 | //! let data = [255, 0, 0, 255, 0, 0, 0, 255]; // An array containing a RGBA sequence. First pixel is red and second pixel is black. |
| 89 | //! writer.write_image_data(&data).unwrap(); // Save |
| 90 | //! |
| 91 | //! // We can add a tEXt/zTXt/iTXt at any point before the encoder is dropped from scope. These chunks will be at the end of the png file. |
| 92 | //! let tail_ztxt_chunk = ZTXtChunk::new("Comment" .to_string(), "A zTXt chunk after the image data." .to_string()); |
| 93 | //! writer.write_text_chunk(&tail_ztxt_chunk).unwrap(); |
| 94 | //! |
| 95 | //! // The fields of the text chunk are public, so they can be mutated before being written to the file. |
| 96 | //! let mut tail_itxt_chunk = ITXtChunk::new("Author" .to_string(), "सायंतन खान" .to_string()); |
| 97 | //! tail_itxt_chunk.compressed = true; |
| 98 | //! tail_itxt_chunk.language_tag = "hi" .to_string(); |
| 99 | //! tail_itxt_chunk.translated_keyword = "लेखक" .to_string(); |
| 100 | //! writer.write_text_chunk(&tail_itxt_chunk).unwrap(); |
| 101 | //! ``` |
| 102 | |
| 103 | #![warn (missing_docs)] |
| 104 | |
| 105 | use crate::{chunk, encoder, DecodingError, EncodingError}; |
| 106 | use fdeflate::BoundedDecompressionError; |
| 107 | use flate2::write::ZlibEncoder; |
| 108 | use flate2::Compression; |
| 109 | use std::{convert::TryFrom, io::Write}; |
| 110 | |
| 111 | /// Default decompression limit for compressed text chunks. |
| 112 | pub const DECOMPRESSION_LIMIT: usize = 2097152; // 2 MiB |
| 113 | |
| 114 | /// Text encoding errors that is wrapped by the standard EncodingError type |
| 115 | #[derive (Debug, Clone, Copy)] |
| 116 | pub(crate) enum TextEncodingError { |
| 117 | /// Unrepresentable characters in string |
| 118 | Unrepresentable, |
| 119 | /// Keyword longer than 79 bytes or empty |
| 120 | InvalidKeywordSize, |
| 121 | /// Error encountered while compressing text |
| 122 | CompressionError, |
| 123 | } |
| 124 | |
| 125 | /// Text decoding error that is wrapped by the standard DecodingError type |
| 126 | #[derive (Debug, Clone, Copy)] |
| 127 | pub(crate) enum TextDecodingError { |
| 128 | /// Unrepresentable characters in string |
| 129 | Unrepresentable, |
| 130 | /// Keyword longer than 79 bytes or empty |
| 131 | InvalidKeywordSize, |
| 132 | /// Missing null separator |
| 133 | MissingNullSeparator, |
| 134 | /// Compressed text cannot be uncompressed |
| 135 | InflationError, |
| 136 | /// Needs more space to decompress |
| 137 | OutOfDecompressionSpace, |
| 138 | /// Using an unspecified value for the compression method |
| 139 | InvalidCompressionMethod, |
| 140 | /// Using a byte that is not 0 or 255 as compression flag in iTXt chunk |
| 141 | InvalidCompressionFlag, |
| 142 | /// Missing the compression flag |
| 143 | MissingCompressionFlag, |
| 144 | } |
| 145 | |
| 146 | /// A generalized text chunk trait |
| 147 | pub trait EncodableTextChunk { |
| 148 | /// Encode text chunk as `Vec<u8>` to a `Write` |
| 149 | fn encode<W: Write>(&self, w: &mut W) -> Result<(), EncodingError>; |
| 150 | } |
| 151 | |
| 152 | /// Struct representing a tEXt chunk |
| 153 | #[derive (Clone, Debug, PartialEq, Eq)] |
| 154 | pub struct TEXtChunk { |
| 155 | /// Keyword field of the tEXt chunk. Needs to be between 1-79 bytes when encoded as Latin-1. |
| 156 | pub keyword: String, |
| 157 | /// Text field of tEXt chunk. Can be at most 2GB. |
| 158 | pub text: String, |
| 159 | } |
| 160 | |
| 161 | fn decode_iso_8859_1(text: &[u8]) -> String { |
| 162 | text.iter().map(|&b: u8| b as char).collect() |
| 163 | } |
| 164 | |
| 165 | pub(crate) fn encode_iso_8859_1(text: &str) -> Result<Vec<u8>, TextEncodingError> { |
| 166 | encode_iso_8859_1_iter(text).collect() |
| 167 | } |
| 168 | |
| 169 | fn encode_iso_8859_1_into(buf: &mut Vec<u8>, text: &str) -> Result<(), TextEncodingError> { |
| 170 | for b: Result in encode_iso_8859_1_iter(text) { |
| 171 | buf.push(b?); |
| 172 | } |
| 173 | Ok(()) |
| 174 | } |
| 175 | |
| 176 | fn encode_iso_8859_1_iter(text: &str) -> impl Iterator<Item = Result<u8, TextEncodingError>> + '_ { |
| 177 | textChars<'_>.chars() |
| 178 | .map(|c: char| u8::try_from(c as u32).map_err(|_| TextEncodingError::Unrepresentable)) |
| 179 | } |
| 180 | |
| 181 | fn decode_ascii(text: &[u8]) -> Result<&str, TextDecodingError> { |
| 182 | if text.is_ascii() { |
| 183 | // `from_utf8` cannot panic because we're already checked that `text` is ASCII-7. |
| 184 | // And this is the only safe way to get ASCII-7 string from `&[u8]`. |
| 185 | Ok(std::str::from_utf8(text).expect(msg:"unreachable" )) |
| 186 | } else { |
| 187 | Err(TextDecodingError::Unrepresentable) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | impl TEXtChunk { |
| 192 | /// Constructs a new TEXtChunk. |
| 193 | /// Not sure whether it should take &str or String. |
| 194 | pub fn new(keyword: impl Into<String>, text: impl Into<String>) -> Self { |
| 195 | Self { |
| 196 | keyword: keyword.into(), |
| 197 | text: text.into(), |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | /// Decodes a slice of bytes to a String using Latin-1 decoding. |
| 202 | /// The decoder runs in strict mode, and any decoding errors are passed along to the caller. |
| 203 | pub(crate) fn decode( |
| 204 | keyword_slice: &[u8], |
| 205 | text_slice: &[u8], |
| 206 | ) -> Result<Self, TextDecodingError> { |
| 207 | if keyword_slice.is_empty() || keyword_slice.len() > 79 { |
| 208 | return Err(TextDecodingError::InvalidKeywordSize); |
| 209 | } |
| 210 | |
| 211 | Ok(Self { |
| 212 | keyword: decode_iso_8859_1(keyword_slice), |
| 213 | text: decode_iso_8859_1(text_slice), |
| 214 | }) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | impl EncodableTextChunk for TEXtChunk { |
| 219 | /// Encodes TEXtChunk to a Writer. The keyword and text are separated by a byte of zeroes. |
| 220 | fn encode<W: Write>(&self, w: &mut W) -> Result<(), EncodingError> { |
| 221 | let mut data: Vec = encode_iso_8859_1(&self.keyword)?; |
| 222 | |
| 223 | if data.is_empty() || data.len() > 79 { |
| 224 | return Err(TextEncodingError::InvalidKeywordSize.into()); |
| 225 | } |
| 226 | |
| 227 | data.push(0); |
| 228 | |
| 229 | encode_iso_8859_1_into(&mut data, &self.text)?; |
| 230 | |
| 231 | encoder::write_chunk(w, name:chunk::tEXt, &data) |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | /// Struct representing a zTXt chunk |
| 236 | #[derive (Clone, Debug, PartialEq, Eq)] |
| 237 | pub struct ZTXtChunk { |
| 238 | /// Keyword field of the tEXt chunk. Needs to be between 1-79 bytes when encoded as Latin-1. |
| 239 | pub keyword: String, |
| 240 | /// Text field of zTXt chunk. It is compressed by default, but can be uncompressed if necessary. |
| 241 | text: OptCompressed, |
| 242 | } |
| 243 | |
| 244 | /// Private enum encoding the compressed and uncompressed states of zTXt/iTXt text field. |
| 245 | #[derive (Clone, Debug, PartialEq, Eq)] |
| 246 | enum OptCompressed { |
| 247 | /// Compressed version of text field. Can be at most 2GB. |
| 248 | Compressed(Vec<u8>), |
| 249 | /// Uncompressed text field. |
| 250 | Uncompressed(String), |
| 251 | } |
| 252 | |
| 253 | impl ZTXtChunk { |
| 254 | /// Creates a new ZTXt chunk. |
| 255 | pub fn new(keyword: impl Into<String>, text: impl Into<String>) -> Self { |
| 256 | Self { |
| 257 | keyword: keyword.into(), |
| 258 | text: OptCompressed::Uncompressed(text.into()), |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | pub(crate) fn decode( |
| 263 | keyword_slice: &[u8], |
| 264 | compression_method: u8, |
| 265 | text_slice: &[u8], |
| 266 | ) -> Result<Self, TextDecodingError> { |
| 267 | if keyword_slice.is_empty() || keyword_slice.len() > 79 { |
| 268 | return Err(TextDecodingError::InvalidKeywordSize); |
| 269 | } |
| 270 | |
| 271 | if compression_method != 0 { |
| 272 | return Err(TextDecodingError::InvalidCompressionMethod); |
| 273 | } |
| 274 | |
| 275 | Ok(Self { |
| 276 | keyword: decode_iso_8859_1(keyword_slice), |
| 277 | text: OptCompressed::Compressed(text_slice.to_vec()), |
| 278 | }) |
| 279 | } |
| 280 | |
| 281 | /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `DECOMPRESSION_LIMIT` bytes. |
| 282 | pub fn decompress_text(&mut self) -> Result<(), DecodingError> { |
| 283 | self.decompress_text_with_limit(DECOMPRESSION_LIMIT) |
| 284 | } |
| 285 | |
| 286 | /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `limit` bytes. |
| 287 | pub fn decompress_text_with_limit(&mut self, limit: usize) -> Result<(), DecodingError> { |
| 288 | match &self.text { |
| 289 | OptCompressed::Compressed(v) => { |
| 290 | let uncompressed_raw = match fdeflate::decompress_to_vec_bounded(&v[..], limit) { |
| 291 | Ok(s) => s, |
| 292 | Err(BoundedDecompressionError::OutputTooLarge { .. }) => { |
| 293 | return Err(DecodingError::from( |
| 294 | TextDecodingError::OutOfDecompressionSpace, |
| 295 | )); |
| 296 | } |
| 297 | Err(_) => { |
| 298 | return Err(DecodingError::from(TextDecodingError::InflationError)); |
| 299 | } |
| 300 | }; |
| 301 | self.text = OptCompressed::Uncompressed(decode_iso_8859_1(&uncompressed_raw)); |
| 302 | } |
| 303 | OptCompressed::Uncompressed(_) => {} |
| 304 | }; |
| 305 | Ok(()) |
| 306 | } |
| 307 | |
| 308 | /// Decompresses the inner text, and returns it as a `String`. |
| 309 | /// If decompression uses more the 2MiB, first call decompress with limit, and then this method. |
| 310 | pub fn get_text(&self) -> Result<String, DecodingError> { |
| 311 | match &self.text { |
| 312 | OptCompressed::Compressed(v) => { |
| 313 | let uncompressed_raw = fdeflate::decompress_to_vec(v) |
| 314 | .map_err(|_| DecodingError::from(TextDecodingError::InflationError))?; |
| 315 | Ok(decode_iso_8859_1(&uncompressed_raw)) |
| 316 | } |
| 317 | OptCompressed::Uncompressed(s) => Ok(s.clone()), |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | /// Compresses the inner text, mutating its own state. |
| 322 | pub fn compress_text(&mut self) -> Result<(), EncodingError> { |
| 323 | match &self.text { |
| 324 | OptCompressed::Uncompressed(s) => { |
| 325 | let uncompressed_raw = encode_iso_8859_1(s)?; |
| 326 | let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); |
| 327 | encoder |
| 328 | .write_all(&uncompressed_raw) |
| 329 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 330 | self.text = OptCompressed::Compressed( |
| 331 | encoder |
| 332 | .finish() |
| 333 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?, |
| 334 | ); |
| 335 | } |
| 336 | OptCompressed::Compressed(_) => {} |
| 337 | } |
| 338 | |
| 339 | Ok(()) |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | impl EncodableTextChunk for ZTXtChunk { |
| 344 | fn encode<W: Write>(&self, w: &mut W) -> Result<(), EncodingError> { |
| 345 | let mut data = encode_iso_8859_1(&self.keyword)?; |
| 346 | |
| 347 | if data.is_empty() || data.len() > 79 { |
| 348 | return Err(TextEncodingError::InvalidKeywordSize.into()); |
| 349 | } |
| 350 | |
| 351 | // Null separator |
| 352 | data.push(0); |
| 353 | |
| 354 | // Compression method: the only valid value is 0, as of 2021. |
| 355 | data.push(0); |
| 356 | |
| 357 | match &self.text { |
| 358 | OptCompressed::Compressed(v) => { |
| 359 | data.extend_from_slice(&v[..]); |
| 360 | } |
| 361 | OptCompressed::Uncompressed(s) => { |
| 362 | // This code may have a bug. Check for correctness. |
| 363 | let uncompressed_raw = encode_iso_8859_1(s)?; |
| 364 | let mut encoder = ZlibEncoder::new(data, Compression::fast()); |
| 365 | encoder |
| 366 | .write_all(&uncompressed_raw) |
| 367 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 368 | data = encoder |
| 369 | .finish() |
| 370 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 371 | } |
| 372 | }; |
| 373 | |
| 374 | encoder::write_chunk(w, chunk::zTXt, &data) |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | /// Struct encoding an iTXt chunk |
| 379 | #[derive (Clone, Debug, PartialEq, Eq)] |
| 380 | pub struct ITXtChunk { |
| 381 | /// The keyword field. This needs to be between 1-79 bytes when encoded as Latin-1. |
| 382 | pub keyword: String, |
| 383 | /// Indicates whether the text will be (or was) compressed in the PNG. |
| 384 | pub compressed: bool, |
| 385 | /// A hyphen separated list of languages that the keyword is translated to. This is ASCII-7 encoded. |
| 386 | pub language_tag: String, |
| 387 | /// Translated keyword. This is UTF-8 encoded. |
| 388 | pub translated_keyword: String, |
| 389 | /// Text field of iTXt chunk. It is compressed by default, but can be uncompressed if necessary. |
| 390 | text: OptCompressed, |
| 391 | } |
| 392 | |
| 393 | impl ITXtChunk { |
| 394 | /// Constructs a new iTXt chunk. Leaves all but keyword and text to default values. |
| 395 | pub fn new(keyword: impl Into<String>, text: impl Into<String>) -> Self { |
| 396 | Self { |
| 397 | keyword: keyword.into(), |
| 398 | compressed: false, |
| 399 | language_tag: "" .to_string(), |
| 400 | translated_keyword: "" .to_string(), |
| 401 | text: OptCompressed::Uncompressed(text.into()), |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | pub(crate) fn decode( |
| 406 | keyword_slice: &[u8], |
| 407 | compression_flag: u8, |
| 408 | compression_method: u8, |
| 409 | language_tag_slice: &[u8], |
| 410 | translated_keyword_slice: &[u8], |
| 411 | text_slice: &[u8], |
| 412 | ) -> Result<Self, TextDecodingError> { |
| 413 | if keyword_slice.is_empty() || keyword_slice.len() > 79 { |
| 414 | return Err(TextDecodingError::InvalidKeywordSize); |
| 415 | } |
| 416 | let keyword = decode_iso_8859_1(keyword_slice); |
| 417 | |
| 418 | let compressed = match compression_flag { |
| 419 | 0 => false, |
| 420 | 1 => true, |
| 421 | _ => return Err(TextDecodingError::InvalidCompressionFlag), |
| 422 | }; |
| 423 | |
| 424 | if compressed && compression_method != 0 { |
| 425 | return Err(TextDecodingError::InvalidCompressionMethod); |
| 426 | } |
| 427 | |
| 428 | let language_tag = decode_ascii(language_tag_slice)?.to_owned(); |
| 429 | |
| 430 | let translated_keyword = std::str::from_utf8(translated_keyword_slice) |
| 431 | .map_err(|_| TextDecodingError::Unrepresentable)? |
| 432 | .to_string(); |
| 433 | let text = if compressed { |
| 434 | OptCompressed::Compressed(text_slice.to_vec()) |
| 435 | } else { |
| 436 | OptCompressed::Uncompressed( |
| 437 | String::from_utf8(text_slice.to_vec()) |
| 438 | .map_err(|_| TextDecodingError::Unrepresentable)?, |
| 439 | ) |
| 440 | }; |
| 441 | |
| 442 | Ok(Self { |
| 443 | keyword, |
| 444 | compressed, |
| 445 | language_tag, |
| 446 | translated_keyword, |
| 447 | text, |
| 448 | }) |
| 449 | } |
| 450 | |
| 451 | /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `DECOMPRESSION_LIMIT` bytes. |
| 452 | pub fn decompress_text(&mut self) -> Result<(), DecodingError> { |
| 453 | self.decompress_text_with_limit(DECOMPRESSION_LIMIT) |
| 454 | } |
| 455 | |
| 456 | /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `limit` bytes. |
| 457 | pub fn decompress_text_with_limit(&mut self, limit: usize) -> Result<(), DecodingError> { |
| 458 | match &self.text { |
| 459 | OptCompressed::Compressed(v) => { |
| 460 | let uncompressed_raw = match fdeflate::decompress_to_vec_bounded(v, limit) { |
| 461 | Ok(s) => s, |
| 462 | Err(BoundedDecompressionError::OutputTooLarge { .. }) => { |
| 463 | return Err(DecodingError::from( |
| 464 | TextDecodingError::OutOfDecompressionSpace, |
| 465 | )); |
| 466 | } |
| 467 | Err(_) => { |
| 468 | return Err(DecodingError::from(TextDecodingError::InflationError)); |
| 469 | } |
| 470 | }; |
| 471 | self.text = OptCompressed::Uncompressed( |
| 472 | String::from_utf8(uncompressed_raw) |
| 473 | .map_err(|_| TextDecodingError::Unrepresentable)?, |
| 474 | ); |
| 475 | } |
| 476 | OptCompressed::Uncompressed(_) => {} |
| 477 | }; |
| 478 | Ok(()) |
| 479 | } |
| 480 | |
| 481 | /// Decompresses the inner text, and returns it as a `String`. |
| 482 | /// If decompression takes more than 2 MiB, try `decompress_text_with_limit` followed by this method. |
| 483 | pub fn get_text(&self) -> Result<String, DecodingError> { |
| 484 | match &self.text { |
| 485 | OptCompressed::Compressed(v) => { |
| 486 | let uncompressed_raw = fdeflate::decompress_to_vec(v) |
| 487 | .map_err(|_| DecodingError::from(TextDecodingError::InflationError))?; |
| 488 | String::from_utf8(uncompressed_raw) |
| 489 | .map_err(|_| TextDecodingError::Unrepresentable.into()) |
| 490 | } |
| 491 | OptCompressed::Uncompressed(s) => Ok(s.clone()), |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | /// Compresses the inner text, mutating its own state. |
| 496 | pub fn compress_text(&mut self) -> Result<(), EncodingError> { |
| 497 | match &self.text { |
| 498 | OptCompressed::Uncompressed(s) => { |
| 499 | let uncompressed_raw = s.as_bytes(); |
| 500 | let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); |
| 501 | encoder |
| 502 | .write_all(uncompressed_raw) |
| 503 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 504 | self.text = OptCompressed::Compressed( |
| 505 | encoder |
| 506 | .finish() |
| 507 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?, |
| 508 | ); |
| 509 | } |
| 510 | OptCompressed::Compressed(_) => {} |
| 511 | } |
| 512 | |
| 513 | Ok(()) |
| 514 | } |
| 515 | } |
| 516 | |
| 517 | impl EncodableTextChunk for ITXtChunk { |
| 518 | fn encode<W: Write>(&self, w: &mut W) -> Result<(), EncodingError> { |
| 519 | // Keyword |
| 520 | let mut data = encode_iso_8859_1(&self.keyword)?; |
| 521 | |
| 522 | if data.is_empty() || data.len() > 79 { |
| 523 | return Err(TextEncodingError::InvalidKeywordSize.into()); |
| 524 | } |
| 525 | |
| 526 | // Null separator |
| 527 | data.push(0); |
| 528 | |
| 529 | // Compression flag |
| 530 | if self.compressed { |
| 531 | data.push(1); |
| 532 | } else { |
| 533 | data.push(0); |
| 534 | } |
| 535 | |
| 536 | // Compression method |
| 537 | data.push(0); |
| 538 | |
| 539 | // Language tag |
| 540 | if !self.language_tag.is_ascii() { |
| 541 | return Err(EncodingError::from(TextEncodingError::Unrepresentable)); |
| 542 | } |
| 543 | data.extend(self.language_tag.as_bytes()); |
| 544 | |
| 545 | // Null separator |
| 546 | data.push(0); |
| 547 | |
| 548 | // Translated keyword |
| 549 | data.extend_from_slice(self.translated_keyword.as_bytes()); |
| 550 | |
| 551 | // Null separator |
| 552 | data.push(0); |
| 553 | |
| 554 | // Text |
| 555 | if self.compressed { |
| 556 | match &self.text { |
| 557 | OptCompressed::Compressed(v) => { |
| 558 | data.extend_from_slice(&v[..]); |
| 559 | } |
| 560 | OptCompressed::Uncompressed(s) => { |
| 561 | let uncompressed_raw = s.as_bytes(); |
| 562 | let mut encoder = ZlibEncoder::new(data, Compression::fast()); |
| 563 | encoder |
| 564 | .write_all(uncompressed_raw) |
| 565 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 566 | data = encoder |
| 567 | .finish() |
| 568 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 569 | } |
| 570 | } |
| 571 | } else { |
| 572 | match &self.text { |
| 573 | OptCompressed::Compressed(v) => { |
| 574 | let uncompressed_raw = fdeflate::decompress_to_vec(v) |
| 575 | .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; |
| 576 | data.extend_from_slice(&uncompressed_raw[..]); |
| 577 | } |
| 578 | OptCompressed::Uncompressed(s) => { |
| 579 | data.extend_from_slice(s.as_bytes()); |
| 580 | } |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | encoder::write_chunk(w, chunk::iTXt, &data) |
| 585 | } |
| 586 | } |
| 587 | |