Skip to content

Commit b811988

Browse files
committed
Working on writing tags
1 parent b49d74b commit b811988

File tree

3 files changed

+252
-0
lines changed

3 files changed

+252
-0
lines changed

src/jpeg.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,197 @@ impl JPEGReader {
4141

4242
Ok(HashMap::new())
4343
}
44+
45+
pub fn write_iptc(
46+
buffer: &Vec<u8>,
47+
data: &HashMap<IPTCTag, String>,
48+
) -> Result<Vec<u8>, Box<dyn Error>> {
49+
let mut new_buffer = Vec::new();
50+
51+
// Copy the initial JPEG marker (SOI)
52+
if buffer.len() < 2 || buffer[0] != 0xFF || buffer[1] != 0xD8 {
53+
return Err("Not a valid JPEG file".into());
54+
}
55+
new_buffer.extend_from_slice(&buffer[0..2]);
56+
let mut offset = 2;
57+
58+
// Convert IPTC data to binary format first
59+
let iptc_data = Self::convert_iptc_to_binary(data)?;
60+
let mut found_app13 = false;
61+
let mut inserted_app13 = false;
62+
63+
// Copy segments until we find SOS
64+
while offset + 1 < buffer.len() {
65+
// Every JPEG segment must start with 0xFF
66+
if buffer[offset] != 0xFF {
67+
offset += 1;
68+
continue;
69+
}
70+
71+
let marker = buffer[offset + 1];
72+
73+
// Skip empty markers
74+
if marker == 0xFF {
75+
offset += 1;
76+
continue;
77+
}
78+
79+
// For markers without length field
80+
if marker == 0x00 || marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7) {
81+
new_buffer.extend_from_slice(&buffer[offset..offset + 2]);
82+
offset += 2;
83+
continue;
84+
}
85+
86+
// End of image marker
87+
if marker == 0xD9 {
88+
new_buffer.extend_from_slice(&buffer[offset..offset + 2]);
89+
break;
90+
}
91+
92+
// Start of scan marker - copy the rest of the file
93+
if marker == 0xDA {
94+
// If we haven't inserted APP13 yet, do it now
95+
if !found_app13 && !inserted_app13 {
96+
// Write APP13 marker
97+
new_buffer.extend_from_slice(&[0xFF, 0xED]);
98+
// Write length (including length bytes)
99+
let total_length = (iptc_data.len() + 2) as u16;
100+
new_buffer.push((total_length >> 8) as u8);
101+
new_buffer.push(total_length as u8);
102+
// Write IPTC data
103+
new_buffer.extend_from_slice(&iptc_data);
104+
}
105+
106+
// Copy SOS marker and all remaining data
107+
new_buffer.extend_from_slice(&buffer[offset..]);
108+
break;
109+
}
110+
111+
// Check if we can read the length
112+
if offset + 3 >= buffer.len() {
113+
new_buffer.extend_from_slice(&buffer[offset..]);
114+
break;
115+
}
116+
117+
let length = buffer.read_u16be(offset + 2) as usize;
118+
119+
// Validate segment length
120+
if length < 2 || offset + 2 + length > buffer.len() {
121+
new_buffer.extend_from_slice(&buffer[offset..]);
122+
break;
123+
}
124+
125+
// If this is APP13, replace it with our new data
126+
if marker == 0xED {
127+
found_app13 = true;
128+
// Write APP13 marker
129+
new_buffer.extend_from_slice(&[0xFF, 0xED]);
130+
// Write length (including length bytes)
131+
let total_length = (iptc_data.len() + 2) as u16;
132+
new_buffer.push((total_length >> 8) as u8);
133+
new_buffer.push(total_length as u8);
134+
// Write IPTC data
135+
new_buffer.extend_from_slice(&iptc_data);
136+
offset += 2 + length;
137+
} else {
138+
// If we haven't found APP13 and this is after APP0/APP1 but before other segments,
139+
// insert our APP13 data here
140+
if !found_app13 && !inserted_app13 && marker > 0xE1 {
141+
// Write APP13 marker
142+
new_buffer.extend_from_slice(&[0xFF, 0xED]);
143+
// Write length (including length bytes)
144+
let total_length = (iptc_data.len() + 2) as u16;
145+
new_buffer.push((total_length >> 8) as u8);
146+
new_buffer.push(total_length as u8);
147+
// Write IPTC data
148+
new_buffer.extend_from_slice(&iptc_data);
149+
inserted_app13 = true;
150+
}
151+
152+
// Copy the marker and its data
153+
new_buffer.extend_from_slice(&buffer[offset..offset + 2 + length]);
154+
offset += 2 + length;
155+
}
156+
}
157+
158+
Ok(new_buffer)
159+
}
160+
161+
fn convert_iptc_to_binary(data: &HashMap<IPTCTag, String>) -> Result<Vec<u8>, Box<dyn Error>> {
162+
let mut binary = Vec::new();
163+
164+
// Add Photoshop header
165+
binary.extend_from_slice(b"Photoshop 3.0\0");
166+
binary.push(0x00); // Pad to even length
167+
168+
// Add 8BIM marker and IPTC block
169+
let mut iptc_block = Vec::new();
170+
171+
// Add IPTC data
172+
for (tag, value) in data {
173+
if let Some((record, dataset)) = Self::get_record_dataset(tag) {
174+
// Field delimiter
175+
iptc_block.push(0x1C);
176+
177+
// Record number and dataset number
178+
iptc_block.push(record);
179+
iptc_block.push(dataset);
180+
181+
// Value length (big endian)
182+
let value_bytes = value.as_bytes();
183+
let value_len = value_bytes.len() as u16;
184+
iptc_block.push((value_len >> 8) as u8);
185+
iptc_block.push(value_len as u8);
186+
187+
// Value
188+
iptc_block.extend_from_slice(value_bytes);
189+
}
190+
}
191+
192+
// Add 8BIM marker
193+
binary.extend_from_slice(b"8BIM");
194+
195+
// Resource ID for IPTC (0x0404)
196+
binary.extend_from_slice(&[0x04, 0x04]);
197+
198+
// Empty name (padded to even length)
199+
binary.push(0x00);
200+
binary.push(0x00);
201+
202+
// Block size (big endian)
203+
let block_size = iptc_block.len() as u32;
204+
binary.extend_from_slice(&[
205+
(block_size >> 24) as u8,
206+
(block_size >> 16) as u8,
207+
(block_size >> 8) as u8,
208+
block_size as u8,
209+
]);
210+
211+
// Add the IPTC block
212+
binary.extend_from_slice(&iptc_block);
213+
214+
// Pad to even length if needed
215+
if binary.len() % 2 != 0 {
216+
binary.push(0x00);
217+
}
218+
219+
Ok(binary)
220+
}
221+
222+
fn get_record_dataset(tag: &IPTCTag) -> Option<(u8, u8)> {
223+
match tag {
224+
IPTCTag::City => Some((2, 90)),
225+
IPTCTag::Keywords => Some((2, 25)),
226+
IPTCTag::ByLine => Some((2, 80)),
227+
IPTCTag::Caption => Some((2, 120)),
228+
IPTCTag::CopyrightNotice => Some((2, 116)),
229+
IPTCTag::Credit => Some((2, 110)),
230+
IPTCTag::Headline => Some((2, 105)),
231+
IPTCTag::ObjectName => Some((2, 5)),
232+
IPTCTag::Source => Some((2, 115)),
233+
IPTCTag::Urgency => Some((2, 10)),
234+
_ => None,
235+
}
236+
}
44237
}

src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ impl IPTC {
5454
returned_tag.unwrap().clone()
5555
}
5656

57+
pub fn set_tag(&mut self, tag: IPTCTag, value: &str) {
58+
self.data.insert(tag, value.to_string());
59+
}
60+
61+
pub fn write_to_file(&self, image_path: &Path) -> Result<(), Box<dyn Error>> {
62+
let file = File::open(image_path)?;
63+
let bufreader = BufReader::new(file);
64+
let img_reader = ImageReader::new(bufreader).with_guessed_format()?;
65+
let format = img_reader.format().ok_or("Image format not supported")?;
66+
67+
let file = File::open(image_path)?;
68+
let mut bufreader = BufReader::new(file);
69+
let mut buffer: Vec<u8> = Vec::new();
70+
bufreader.read_to_end(&mut buffer)?;
71+
72+
let new_buffer = if format == ImageFormat::Jpeg {
73+
JPEGReader::write_iptc(&buffer, &self.data)?
74+
} else {
75+
return Err("Writing IPTC data is only supported for JPEG files".into());
76+
};
77+
78+
std::fs::write(image_path, new_buffer)?;
79+
Ok(())
80+
}
81+
5782
pub fn read_from_path(image_path: &Path) -> Result<Self, Box<dyn Error>> {
5883
let file = File::open(image_path)?;
5984
let bufreader = BufReader::new(file);

tests/smiley.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::error::Error;
2+
use std::fs;
23
use std::path::Path;
34

45
use iptc::IPTC;
@@ -32,3 +33,36 @@ fn exiv2_iptc_example() -> Result<(), Box<dyn Error>> {
3233

3334
Ok(())
3435
}
36+
37+
#[test]
38+
fn test_write_iptc() -> Result<(), Box<dyn Error>> {
39+
// Create a copy of the test file so we don't modify the original
40+
let original_path = Path::new("tests/smiley.jpg");
41+
let test_path = Path::new("tests/smiley_write_test.jpg");
42+
let debug_path = Path::new("tests/smiley_debug.jpg");
43+
fs::copy(original_path, test_path)?;
44+
45+
// Read the original IPTC data
46+
let mut iptc = IPTC::read_from_path(&test_path)?;
47+
48+
// Modify some tags
49+
iptc.set_tag(IPTCTag::City, "Oslo");
50+
iptc.set_tag(IPTCTag::Keywords, "New keyword");
51+
52+
// Write the changes
53+
iptc.write_to_file(&test_path)?;
54+
55+
// Make a copy for debugging
56+
fs::copy(test_path, debug_path)?;
57+
58+
// Read back and verify
59+
let new_iptc = IPTC::read_from_path(&test_path)?;
60+
assert_eq!(new_iptc.get(IPTCTag::City), "Oslo");
61+
assert_eq!(new_iptc.get(IPTCTag::Keywords), "New keyword");
62+
63+
// Clean up
64+
fs::remove_file(test_path)?;
65+
fs::remove_file(debug_path)?;
66+
67+
Ok(())
68+
}

0 commit comments

Comments
 (0)