Coverage Report

Created: 2026-01-19 07:25

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