use std::{
fs::File,
io::{BufWriter, Cursor, Seek, Write},
num::NonZeroUsize,
path::Path,
};
use image::ImageFormat;
use sevenz_rust2::{ArchiveEntry, ArchiveWriter};
#[cfg(feature = "comicinfo")]
use crate::comicinfo::{
COMIC_INFO_XML, ComicInfoBuilder, ComicInfoBuilderError,
comic_page_info::{ComicPageInfoBuilder, ComicPageInfoBuilderError},
};
use crate::write::ComicBookWriter;
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
#[non_exhaustive]
pub enum Cb7WriterError {
Image(#[from] image::ImageError),
Io(#[from] std::io::Error),
#[error("the inner 7z inner writer was dropped early")]
S7WriterInnerDroppedEarly,
S7(#[from] sevenz_rust2::Error),
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
SerdeXml(#[from] serde_xml_rs::Error),
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
ComicInfoBuilder(#[from] ComicInfoBuilderError),
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
ComicPageInfoBuilder(#[from] ComicPageInfoBuilderError),
IntCast(#[from] std::num::TryFromIntError),
#[error("cannot transform a `Path` into a `&str`")]
PathToStr,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Cb7WriterImageFormat {
#[default]
Png,
Jpeg,
}
impl From<Cb7WriterImageFormat> for image::ImageFormat {
fn from(value: Cb7WriterImageFormat) -> Self {
match value {
Cb7WriterImageFormat::Jpeg => Self::Jpeg,
Cb7WriterImageFormat::Png => Self::Png,
}
}
}
#[derive(derive_more::Debug)]
pub struct Cb7Writer<W>
where
W: Write + Seek,
{
s7_inner: Option<ArchiveWriter<W>>,
count: usize,
suffix: Option<String>,
width: usize,
images_format: Cb7WriterImageFormat,
#[cfg(feature = "comicinfo")]
comicinfo_builder: Option<ComicInfoBuilder>,
#[cfg(feature = "comicinfo")]
auto_double_page: bool,
}
impl<W> Cb7Writer<W>
where
W: Write + Seek,
{
pub fn new(writer: W) -> Result<Self, sevenz_rust2::Error> {
Ok(Self::from_archive_writer(ArchiveWriter::new(writer)?))
}
pub fn from_archive_writer(writer: ArchiveWriter<W>) -> Self {
Self {
s7_inner: Some(writer),
count: 1,
suffix: None,
width: 4,
images_format: Default::default(),
#[cfg(feature = "comicinfo")]
comicinfo_builder: None,
#[cfg(feature = "comicinfo")]
auto_double_page: true,
}
}
pub fn suffix(mut self, suffix: String) -> Self {
self.suffix = Some(suffix);
self
}
pub fn no_suffix(mut self) -> Self {
let _ = self.suffix.take();
self
}
pub fn width(mut self, width: NonZeroUsize) -> Self {
self.width = width.into();
self
}
fn s7_inner_(&mut self) -> Result<&mut ArchiveWriter<W>, Cb7WriterError> {
self.s7_inner
.as_mut()
.ok_or(Cb7WriterError::S7WriterInnerDroppedEarly)
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
pub fn auto_double_page(mut self, auto_double_page: bool) -> Self {
self.auto_double_page = auto_double_page;
self
}
pub fn finish(mut self) -> Result<W, Cb7WriterError> {
#[cfg(feature = "comicinfo")]
{
self.write_comicinfo()?;
}
let write = self
.s7_inner
.take()
.ok_or(Cb7WriterError::S7WriterInnerDroppedEarly)?
.finish()?;
Ok(write)
}
#[cfg(feature = "comicinfo")]
fn write_comicinfo(&mut self) -> Result<(), Cb7WriterError> {
if let Some(comicinfo) = self.comicinfo_builder.take() {
let mut buf = tempfile::spooled_tempfile(1024);
serde_xml_rs::to_writer(&mut buf, &comicinfo.build()?)?;
buf.rewind()?;
self.add_file(COMIC_INFO_XML, buf)?;
} else {
#[cfg(feature = "log")]
{
log::warn!("no comic info found... moving on");
}
}
Ok(())
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
pub fn set_comicinfo_builder(mut self, builder: ComicInfoBuilder) -> Self {
self.comicinfo_builder = Some(builder);
self
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
pub fn take_comicinfo_builder(&mut self) -> Option<ComicInfoBuilder> {
self.comicinfo_builder.take()
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
pub fn unset_comicinfo_builder(mut self) -> Self {
let _ = self.take_comicinfo_builder();
self
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
pub fn get_comicinfo_builder(&mut self) -> Option<&ComicInfoBuilder> {
self.comicinfo_builder.as_ref()
}
#[cfg(feature = "comicinfo")]
fn get_current_page_info_builder(&self) -> ComicPageInfoBuilder {
let mut builder = ComicPageInfoBuilder::default();
builder.image(self.count);
builder
}
fn inner_add_image(&mut self, add_page: AddPage) -> Result<(), Cb7WriterError> {
let (mut img_buf, filename): (Cursor<Vec<u8>>, String) = {
let width = self.width;
let mut buf = Cursor::new(Vec::<u8>::new());
let name = if matches!(add_page._format, Some(ImageFormat::Gif)) {
add_page.image.write_to(&mut buf, ImageFormat::Gif)?;
format!(
"{}{:0>width$}.gif",
self.suffix.as_deref().unwrap_or(""),
self.count
)
} else {
add_page
.image
.write_to(&mut buf, self.images_format.into())?;
format!(
"{}{:0>width$}.{}",
self.suffix.as_deref().unwrap_or(""),
self.count,
match self.images_format {
Cb7WriterImageFormat::Jpeg => "jpg",
Cb7WriterImageFormat::Png => "png",
}
)
};
(buf, name)
};
img_buf.rewind()?;
{
let entry = ArchiveEntry::new_file(&filename);
self.s7_inner_()?.push_archive_entry(entry, Some(img_buf))?;
}
#[cfg(feature = "comicinfo")]
{
if self.comicinfo_builder.is_some() {
let mut page_builder = self.get_current_page_info_builder();
page_builder
.image(self.count)
.image_height(NonZeroUsize::new(add_page.image.height().try_into()?))
.image_width(NonZeroUsize::new(add_page.image.width().try_into()?));
if self.auto_double_page {
page_builder.double_page(add_page.image.width() > add_page.image.height());
}
if let Some(bookmark) = add_page._bookmark {
page_builder.bookmark(bookmark);
}
if !add_page._page_type.is_empty() {
page_builder.type_(add_page._page_type);
}
if let Some(builder) = self.comicinfo_builder.as_mut() {
builder.add_page(page_builder.build()?);
}
}
}
self.count += 1;
Ok(())
}
}
impl Cb7Writer<BufWriter<File>> {
pub fn create<P: AsRef<Path>>(path: P) -> Result<Self, sevenz_rust2::Error> {
Self::new(BufWriter::new(File::create(path)?))
}
}
impl<W> Drop for Cb7Writer<W>
where
W: Write + Seek,
{
fn drop(&mut self) {
#[cfg(feature = "comicinfo")]
{
let _a = self.write_comicinfo();
#[cfg(feature = "log")]
{
let _ = _a.inspect_err(|err| {
log::error!("cannot write the `ComicInfo.xml` on drop : [{}]", err);
});
}
}
if let Some(inner) = self.s7_inner.take() {
let _res = inner.finish();
#[cfg(feature = "log")]
{
let _ = _res.inspect_err(|err| {
log::error!("cannot finish 7z writer [{}]", err);
});
}
}
}
}
struct AddPage {
image: image::DynamicImage,
_format: Option<image::ImageFormat>,
#[cfg(feature = "comicinfo")]
_page_type: Vec<crate::comicinfo::comic_page_type::ComicPageType>,
#[cfg(feature = "comicinfo")]
_bookmark: Option<String>,
}
impl<W> ComicBookWriter for Cb7Writer<W>
where
W: Write + Seek,
{
type Error = Cb7WriterError;
fn add_page(
&mut self,
image: image::DynamicImage,
format: Option<image::ImageFormat>,
) -> Result<(), Self::Error> {
self.inner_add_image(AddPage {
image,
_format: format,
#[cfg(feature = "comicinfo")]
_page_type: Vec::new(),
#[cfg(feature = "comicinfo")]
_bookmark: None,
})
}
#[cfg(feature = "comicinfo")]
#[cfg_attr(docsrs, doc(feature = "comicinfo"))]
fn add_page_with_metadata(
&mut self,
image: image::DynamicImage,
_format: Option<image::ImageFormat>,
_page_type: Vec<crate::comicinfo::comic_page_type::ComicPageType>,
_bookmark: Option<String>,
) -> Result<(), Self::Error> {
self.inner_add_image(AddPage {
image,
_format,
#[cfg(feature = "comicinfo")]
_page_type,
#[cfg(feature = "comicinfo")]
_bookmark,
})
}
fn add_file<P, R>(&mut self, path: P, file: R) -> Result<(), Self::Error>
where
P: AsRef<std::path::Path>,
R: std::io::Read,
{
let header =
ArchiveEntry::new_file(path.as_ref().to_str().ok_or(Cb7WriterError::PathToStr)?);
self.s7_inner_()?.push_archive_entry(header, Some(file))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs::read_dir,
io::{self, BufReader, BufWriter, Seek, Write},
num::NonZero,
path::Path,
};
use crate::{
read::{ComicBookReader, cb7::Cb7Reader},
write::ComicBookWriter,
};
#[test]
fn test_write() -> anyhow::Result<()> {
let to_import = read_dir("test-data/images/no-order")?.collect::<io::Result<Vec<_>>>()?;
let mut file_to_use = tempfile::tempfile()?;
{
let mut writer = Cb7Writer::new(BufWriter::new(&mut file_to_use))?
.width(NonZero::new(4).ok_or(anyhow::anyhow!("Unreachable"))?);
for img in &to_import {
writer.add_page(
image::open(img.path())?,
image::ImageFormat::from_path(img.path()).ok(),
)?;
}
writer.finish()?.flush()?;
}
file_to_use.rewind()?;
{
let reader = Cb7Reader::new(BufReader::new(&mut file_to_use))?;
let images = reader.pages();
assert_eq!(images.len(), to_import.len());
for (index, image) in images.into_iter().enumerate() {
assert_eq!(
format!("{:0>4}", index + 1).as_str(),
Path::new(&image)
.file_prefix()
.and_then(|d| d.to_str())
.ok_or(anyhow::anyhow!("No filename"))?
);
}
}
Ok(())
}
#[cfg(feature = "comicinfo")]
#[test]
fn test_with_comic_info() -> anyhow::Result<()> {
let to_import = read_dir("test-data/images/no-order")?.collect::<io::Result<Vec<_>>>()?;
let mut file_to_use = tempfile::tempfile()?;
let writed_comic_info = {
use fake::{Fake, faker};
use crate::comicinfo::ComicInfoBuilder;
let mut writer = Cb7Writer::new(BufWriter::new(&mut file_to_use))?
.set_comicinfo_builder({
let mut builer = ComicInfoBuilder::default();
builer
.title(faker::name::ja_jp::Title().fake())
.summary(faker::lorem::en::Paragraph(1..3).fake())
.series(faker::name::ja_jp::NameWithTitle().fake())
.writer(faker::name::ja_jp::Name().fake())
.colorist(faker::name::ja_jp::Name().fake())
.translator(faker::name::en::Name().fake())
.language_iso("en".into());
builer
})
.width(NonZero::new(4).ok_or(anyhow::anyhow!("Unreachable"))?);
for img in &to_import {
writer.add_page(
image::open(img.path())?,
image::ImageFormat::from_path(img.path()).ok(),
)?;
}
let comic_info = writer.get_comicinfo_builder().cloned().unwrap().build()?;
writer.finish()?.flush()?;
comic_info
};
file_to_use.rewind()?;
{
use crate::read::comicinfo::GetComicInfo;
let mut reader = Cb7Reader::new(BufReader::new(&mut file_to_use))?;
let images = reader.pages();
assert_eq!(images.len(), to_import.len());
for (index, image) in images.into_iter().enumerate() {
assert_eq!(
format!("{:0>4}", index + 1).as_str(),
Path::new(&image)
.file_prefix()
.and_then(|d| d.to_str())
.ok_or(anyhow::anyhow!("No filename"))?
);
}
assert_eq!(reader.get_comic_info()?, writed_comic_info);
}
Ok(())
}
}