use super::placement::{source_image_transform, source_page_geometry};
use std::hint::black_box;
use std::io::Write;
mod native;
mod route;
pub use native::{C8FontSource, C8FontSources, convert_c8_native_pdf};
pub use route::{convert_document_pdf, uses_native_text};
use super::type3_image::{CheckedType3, Type3Stores, emit_type3, preflight_type3};
use super::{
ApplicationInfoStatus, At, Header, Hnc8Reader, ImageRecord, JpegColor, JpegInfo, OutlineReport,
PageRecord, RawTextCoordinate, Variant, empirical_image_transform, empirical_page_from_pixels,
read_type2_jpeg_info,
};
use crate::fallible::{len_u64, reserve, reserve_exact, usize_from_u32};
use crate::jbig1::{Type0Decoder, Type0Info, read_type0_info};
use crate::jbig2::text::{TextHeaderAnomaly, TextHeaderPolicy};
use crate::pdf::{
BilevelImageSpec, BookmarkView, ImageEncoding, ImagePlacement, ImageSpec,
MAX_PAGE_IMAGE_PLACEMENTS, PageSpec, PdfDocument,
};
use crate::qm::{ContextBank, QmTable};
use crate::{
Bookmark, BookmarkVisitor, Cancellation, ConversionReport, CountingSource, Error, ErrorKind,
Hnc8Stage, Limits, Payload, RangedSource, Result, read_payload,
};
use std::mem::size_of;
#[derive(Default)]
struct ImageBuffers {
contexts: Option<ContextBank>,
payload: Vec<u8>,
type3: Type3Stores,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ComposeOptions {
pub include_bookmarks: bool,
pub text_header_policy: TextHeaderPolicy,
}
#[derive(Clone, Copy, Debug)]
enum CheckedImage {
Type0(Type0Info),
Jpeg(JpegInfo),
Type3,
}
struct OutlineSink<'a, 'b, W: Write, C: Cancellation>(&'a mut PdfDocument<'b, W, C>);
impl<W: Write, C: Cancellation> BookmarkVisitor for OutlineSink<'_, '_, W, C> {
fn visit(&mut self, bookmark: Bookmark) -> crate::Result<()> {
self.0.add_bookmark_with_view(bookmark, BookmarkView::Xyz)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ComposedImage {
pub record: ImageRecord,
pub visible_width: u32,
pub display_width: u32,
pub height: u32,
pub transform: [f64; 6],
pub duplicate_of: Option<u32>,
pub type3_text_header_anomaly: Option<TextHeaderAnomaly>,
checked: CheckedImage,
}
#[derive(Clone, Copy, Debug)]
pub struct ComposePage<'a> {
pub source: PageRecord,
pub output_page: Option<u32>,
pub size: Option<PageSpec>,
pub images: &'a [ComposedImage],
}
pub trait ComposeVisitor {
fn page(&mut self, page: ComposePage<'_>) -> crate::Result<()>;
}
impl ComposeVisitor for () {
fn page(&mut self, _page: ComposePage<'_>) -> crate::Result<()> {
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComposeReport {
pub conversion: ConversionReport,
pub source_variant: Variant,
pub source_pages: u32,
pub output_pages: u32,
pub no_image_pages: u32,
pub type0_images: u64,
pub jpeg_images: u64,
pub type3_images: u64,
pub duplicate_image_records: u64,
pub outline: OutlineReport,
pub application_info: ApplicationInfoStatus,
}
impl At {
fn page(header: Header, page: PageRecord) -> Self {
Self {
variant: Some(header.variant),
page: Some(page.page_number),
image: None,
offset: Some(page.row_offset),
}
}
fn image(self, record: ImageRecord) -> Self {
Self {
image: Some(record.image_number),
offset: Some(record.descriptor_offset),
..self
}
}
}
fn unsupported(reason: &'static str) -> Error {
Error::from(ErrorKind::UnsupportedFormat).because(reason)
}
fn finish_document<S: RangedSource, W: Write, C: Cancellation>(
reader: &mut Hnc8Reader<'_, S, C>,
document: PdfDocument<'_, W, C>,
report: &mut ComposeReport,
at: At,
) -> Result<ConversionReport> {
let read = reader
.application_info_report()
.map_err(|error| container(error, Hnc8Stage::Container))?;
finish_with_application_info(document, report, at, read)
}
fn finish_with_application_info<W: Write, C: Cancellation>(
document: PdfDocument<'_, W, C>,
report: &mut ComposeReport,
at: At,
read: super::ApplicationInfoReport,
) -> Result<ConversionReport> {
report.application_info = read.status;
let info = read.info.unwrap_or_default();
document
.finish_with_info(&[
("CNKI_DOI", info.doi.as_deref()),
("CNKI_URL", info.url.as_deref()),
])
.map_err(at.locator(Hnc8Stage::Pdf))
}
fn container(error: Error, stage: Hnc8Stage) -> Error {
At::NONE.locate(stage, error)
}
fn type0_decode(at: At) -> impl Fn(Error) -> Error {
move |error| at.locate(Hnc8Stage::Decode, error)
}
#[allow(clippy::too_many_arguments)]
fn emit_type0<W, C>(
payload: Payload<'_>,
document: &mut PdfDocument<'_, W, C>,
record: ImageRecord,
info: Type0Info,
table: &QmTable,
contexts: &mut ContextBank,
at: At,
limits: &Limits,
cancellation: &C,
) -> Result<crate::pdf::ImageObject>
where
W: Write,
C: Cancellation,
{
let mut rows = document
.begin_bilevel_image(BilevelImageSpec {
pixel_width: info.width,
pixel_height: info.height,
row_stride: info.dib_stride,
})
.map_err(at.locator(Hnc8Stage::Pdf))?;
let span = record
.type0_span()
.expect("type-0 record checked by composition preflight");
let mut decoder = Type0Decoder::new(
payload,
span,
table,
contexts,
&mut rows,
limits,
cancellation,
)
.map_err(type0_decode(at))?;
while decoder.decode_next_row().map_err(type0_decode(at))? {}
decoder.finish().map_err(type0_decode(at))?;
rows.finish().map_err(at.locator(Hnc8Stage::Pdf))
}
fn verify_repeated_image<S: RangedSource, C: Cancellation>(
source: &mut S,
original: ImageRecord,
repeated: ImageRecord,
at: At,
limits: &Limits,
cancellation: &C,
) -> Result<()> {
if original.record_type != repeated.record_type
|| original.payload.length != repeated.payload.length
{
return Err(at.error(
Hnc8Stage::Headers,
unsupported("repeated image type or length differs"),
));
}
let mut first = [0; 1024];
let mut second = [0; 1024];
let mut offset = 0;
while offset < original.payload.length {
let count = (original.payload.length - offset)
.min(limits.io_chunk_bytes.min(first.len()) as u64) as usize;
crate::read_exact_at(
source,
original.payload.offset + offset,
&mut first[..count],
limits,
cancellation,
)
.map_err(at.locator(Hnc8Stage::Headers))?;
crate::read_exact_at(
source,
repeated.payload.offset + offset,
&mut second[..count],
limits,
cancellation,
)
.map_err(at.locator(Hnc8Stage::Headers))?;
if first[..count] != second[..count] {
return Err(at.with_offset(repeated.payload.offset + offset).error(
Hnc8Stage::Headers,
unsupported("repeated image payload differs"),
));
}
offset += count as u64;
}
Ok(())
}
fn validate(limits: &Limits) -> Result<()> {
limits
.validate()
.map_err(At::NONE.locator(Hnc8Stage::Preflight))
}
fn metadata_bytes(count: u64, element_bytes: u64) -> crate::Result<u64> {
count
.checked_mul(element_bytes)
.ok_or(Error::invalid("page metadata byte count overflows u64"))
}
fn capacity_bytes<T>(capacity: usize) -> u64 {
len_u64(capacity) * size_of::<T>() as u64
}
fn page_vector<T>(count: usize, limits: &Limits, resource: &'static str) -> crate::Result<Vec<T>> {
let bytes = metadata_bytes(len_u64(count), size_of::<T>() as u64)?;
limits.check_allocation(bytes)?;
let mut values = Vec::new();
reserve_exact(
&mut values,
count,
limits.allocation_refused(resource, bytes),
)?;
limits.check_allocation(capacity_bytes::<T>(values.capacity()))?;
Ok(values)
}
fn admit_page_images(
page: PageRecord,
element_bytes: &[usize],
at: At,
limits: &Limits,
) -> Result<usize> {
let count = usize_from_u32(page.image_count);
if count > MAX_PAGE_IMAGE_PLACEMENTS {
return Err(at.locator(Hnc8Stage::Preflight)(Error::limit(
"PDF image placements per page",
MAX_PAGE_IMAGE_PLACEMENTS as u64,
u64::from(page.image_count),
)));
}
for &element in element_bytes {
let bytes = metadata_bytes(u64::from(page.image_count), len_u64(element))
.map_err(at.locator(Hnc8Stage::Preflight))?;
limits
.check_allocation(bytes)
.map_err(at.locator(Hnc8Stage::Preflight))?;
}
Ok(count)
}
fn preflight_image<S: RangedSource, C: Cancellation>(
source: &mut S,
record: ImageRecord,
image_at: At,
table: Option<&QmTable>,
options: ComposeOptions,
limits: &Limits,
cancellation: &C,
) -> Result<(CheckedImage, Option<CheckedType3>, u32, u32, u32)> {
let variant = image_at.variant.expect("a page location names its variant");
Ok(match record.record_type {
0 if variant != Variant::HnB => {
if table.is_none() {
return Err(image_at.error(
Hnc8Stage::Headers,
Error::invalid("type-0 image requires a caller QM table"),
));
}
let info = read_type0_info(
source,
record.type0_span().expect("matched type zero"),
limits,
cancellation,
)
.map_err(image_at.locator(Hnc8Stage::Headers))?;
let display_width = info.width;
(
CheckedImage::Type0(info),
None,
info.width,
display_width,
info.height,
)
}
3 => {
let checked = preflight_type3(
source,
record,
image_at,
options.text_header_policy,
limits,
cancellation,
)?;
let page = checked.page();
let display_width = page.width;
(
CheckedImage::Type3,
Some(checked),
page.width,
display_width,
page.height,
)
}
1 | 2 if record.record_type == 2 || variant != Variant::HnB => {
let info = read_type2_jpeg_info(source, record, limits, cancellation)
.map_err(image_at.locator(Hnc8Stage::Headers))?;
let width = u32::from(info.width);
let height = u32::from(info.height);
(CheckedImage::Jpeg(info), None, width, width, height)
}
_ => {
return Err(image_at.error(Hnc8Stage::Headers, unsupported("image record type")));
}
})
}
#[allow(clippy::too_many_arguments)]
fn emit_image<S, W, C>(
source: &mut S,
document: &mut PdfDocument<'_, W, C>,
image: &mut ComposedImage,
type3: Option<CheckedType3>,
image_at: At,
buffers: &mut ImageBuffers,
table: Option<&QmTable>,
limits: &Limits,
cancellation: &C,
report: &mut ComposeReport,
) -> Result<crate::pdf::ImageObject>
where
S: RangedSource,
W: Write,
C: Cancellation,
{
let record = image.record;
let payload_at = image_at.with_offset(record.payload.offset);
let object = match image.checked {
CheckedImage::Type0(info) => {
if buffers.contexts.is_none() {
buffers.contexts = Some(
ContextBank::new(1024, limits).map_err(image_at.locator(Hnc8Stage::Decode))?,
);
}
let payload = read_payload(
source,
record.payload.offset,
record.payload.length,
&mut buffers.payload,
limits,
cancellation,
)
.map_err(payload_at.locator(Hnc8Stage::Decode))?;
let object = emit_type0(
payload,
document,
record,
info,
table.expect("type-zero table checked"),
buffers.contexts.as_mut().expect("contexts constructed"),
image_at,
limits,
cancellation,
)?;
report.type0_images += 1;
object
}
CheckedImage::Type3 => {
let payload = read_payload(
source,
record.payload.offset,
record.payload.length,
&mut buffers.payload,
limits,
cancellation,
)
.map_err(payload_at.locator(Hnc8Stage::Decode))?;
let (object, anomaly) = emit_type3(
payload,
document,
&mut buffers.type3,
type3.expect("type-3 metadata retained from preflight"),
image_at,
limits,
cancellation,
)?;
image.type3_text_header_anomaly = anomaly;
report.type3_images += 1;
object
}
CheckedImage::Jpeg(info) => {
let encoding = match info.color {
JpegColor::Gray => ImageEncoding::JpegGray8,
JpegColor::Ycbcr => ImageEncoding::JpegRgb8,
};
let spec = ImageSpec {
pixel_width: u32::from(info.width),
pixel_height: u32::from(info.height),
encoding,
};
let object = document
.add_image(source, record.payload.offset, record.payload.length, spec)
.map_err(payload_at.locator(Hnc8Stage::Pdf))?;
report.jpeg_images += 1;
object
}
};
Ok(object)
}
pub fn convert_source_pages_pdf<S, W, V, C>(
source: &mut S,
sink: &mut W,
table: Option<&QmTable>,
visitor: &mut V,
options: ComposeOptions,
limits: &Limits,
cancellation: &C,
) -> Result<ComposeReport>
where
S: RangedSource,
W: Write,
V: ComposeVisitor,
C: Cancellation,
{
validate(limits)?;
let mut input_bytes_read = 0;
let mut counted = CountingSource::new(source, &mut input_bytes_read);
let mut reader = Hnc8Reader::open(&mut counted, limits, cancellation)
.map_err(|error| container(error, Hnc8Stage::Container))?;
let header = reader.header();
let document_at = At {
variant: Some(header.variant),
offset: Some(0),
..At::NONE
};
let mut document = PdfDocument::new(sink, limits, cancellation)
.map_err(document_at.locator(Hnc8Stage::Pdf))?;
let mut report = ComposeReport::new(header);
let include_bookmarks = options.include_bookmarks && header.variant == Variant::HnA;
report.outline.unverified = options.include_bookmarks && !include_bookmarks;
let mut buffers = ImageBuffers::default();
while let Some(page) = reader
.next_page()
.map_err(|error| container(error, Hnc8Stage::Container))?
{
let at = At::page(header, page);
if page.image_count == 0 {
if header.variant != Variant::HnB {
return Err(at.error(
Hnc8Stage::Preflight,
unsupported("image-only output has no image to draw"),
));
}
report.no_image_pages += 1;
visitor
.page(ComposePage {
source: page,
output_page: None,
size: None,
images: &[],
})
.map_err(at.locator(Hnc8Stage::Visitor))?;
continue;
}
if header.variant == Variant::HnB && page.image_count != 1 {
return Err(at.error(
Hnc8Stage::Preflight,
unsupported("HN-B image-bearing rows require exactly one JPEG"),
));
}
let count = admit_page_images(
page,
&[size_of::<ComposedImage>(), size_of::<ImagePlacement>()],
at,
limits,
)?;
let mut coordinates = Vec::new();
let mut page_size = if header.variant == Variant::HnB {
None
} else {
header.page_size
};
if header.variant != Variant::HnB {
let text = super::text::read_coordinates(
reader.source_mut(),
header,
page,
limits,
cancellation,
)
.map_err(|error| container(error, Hnc8Stage::Text))?;
page_size = text.page_size.or(page_size);
coordinates = text.coordinates;
}
if header.variant != Variant::HnB
&& (coordinates.is_empty() || !count.is_multiple_of(coordinates.len()))
{
return Err(at.error(
Hnc8Stage::Text,
unsupported("image descriptor count is not a complete coordinate group"),
));
}
let mut images: Vec<ComposedImage> = page_vector(count, limits, "current-page image plans")
.map_err(at.locator(Hnc8Stage::Preflight))?;
let mut type3_plans: Vec<CheckedType3> = Vec::new();
let mut geometry = page_size
.map(source_page_geometry)
.transpose()
.map_err(at.locator(Hnc8Stage::Geometry))?;
while let Some(record) = reader
.next_image()
.map_err(|error| container(error, Hnc8Stage::Container))?
{
let image_at = at.image(record);
if header.variant != Variant::HnB && images.len() >= coordinates.len() {
let original = images[images.len() % coordinates.len()];
verify_repeated_image(
reader.source_mut(),
original.record,
record,
image_at,
limits,
cancellation,
)?;
images.push(ComposedImage {
record,
duplicate_of: Some(original.record.image_number),
..original
});
continue;
}
let (checked, plan, visible_width, display_width, height) = preflight_image(
reader.source_mut(),
record,
image_at,
table,
options,
limits,
cancellation,
)?;
if let Some(plan) = plan {
let wanted = capacity_bytes::<CheckedType3>(type3_plans.len() + 1);
reserve(
&mut type3_plans,
1,
limits.allocation_refused("current-page type-3 plans", wanted),
)
.map_err(image_at.locator(Hnc8Stage::Preflight))?;
type3_plans.push(plan);
}
if geometry.is_none() {
geometry = Some(
empirical_page_from_pixels(visible_width, height, [0.0, 0.0])
.map_err(image_at.locator(Hnc8Stage::Geometry))?,
);
}
let coordinate = if header.variant == Variant::HnB {
RawTextCoordinate::default()
} else {
coordinates[usize_from_u32(record.image_number - 1)]
};
let page_geometry = geometry.expect("source page or HN-B first image checked");
let mut transform = if header.variant == Variant::HnB {
empirical_image_transform(page_geometry, display_width, height, coordinate)
} else {
source_image_transform(page_geometry, coordinate)
}
.map_err(image_at.locator(Hnc8Stage::Geometry))?;
if record.record_type != 2 {
transform[5] = black_box(transform[5]) + black_box(transform[3]);
transform[3] = -transform[3];
}
images.push(ComposedImage {
record,
visible_width,
display_width,
height,
transform,
checked,
type3_text_header_anomaly: None,
duplicate_of: None,
});
}
drop(coordinates);
let mut placements = page_vector(count, limits, "current-page image placements")
.map_err(at.locator(Hnc8Stage::Preflight))?;
let mut type3_plans = type3_plans.into_iter();
for index in 0..images.len() {
if let Some(original) = images[index].duplicate_of {
images[index].type3_text_header_anomaly =
images[usize_from_u32(original - 1)].type3_text_header_anomaly;
report.duplicate_image_records += 1;
continue;
}
let image = &mut images[index];
let image_at = at.image(image.record);
let plan = match image.checked {
CheckedImage::Type3 => type3_plans.next(),
_ => None,
};
let object = emit_image(
reader.source_mut(),
&mut document,
image,
plan,
image_at,
&mut buffers,
table,
limits,
cancellation,
&mut report,
)?;
placements.push(ImagePlacement {
image: object,
transform: image.transform,
});
}
let size = geometry.expect("positive source image count").size;
report.output_pages = document
.add_placed_page(size, &placements)
.map_err(at.locator(Hnc8Stage::Pdf))?
+ 1;
visitor
.page(ComposePage {
source: page,
output_page: Some(report.output_pages),
size: Some(size),
images: &images,
})
.map_err(at.locator(Hnc8Stage::Visitor))?;
}
if report.output_pages == 0 {
return Err(document_at.error(
Hnc8Stage::Preflight,
unsupported("image-only output has no image to draw"),
));
}
if include_bookmarks {
debug_assert_eq!(report.output_pages, header.page_count);
report.outline = reader
.visit_bookmarks(
64,
report.output_pages,
|page| Some(page - 1),
&mut OutlineSink(&mut document),
)
.map_err(|error| container(error, Hnc8Stage::Container))?;
}
report.conversion = finish_document(&mut reader, document, &mut report, document_at)?;
report.conversion.input_bytes_read = input_bytes_read;
Ok(report)
}
#[cfg(test)]
mod tests;
impl ComposeReport {
fn new(header: Header) -> Self {
Self {
conversion: ConversionReport::default(),
source_variant: header.variant,
source_pages: header.page_count,
output_pages: 0,
no_image_pages: 0,
type0_images: 0,
jpeg_images: 0,
type3_images: 0,
duplicate_image_records: 0,
outline: OutlineReport::default(),
application_info: ApplicationInfoStatus::Absent,
}
}
}