Add Rust PNG codec backend for testing and remove Skia PNG encoder Replace the legacy Skia-based PNG backend in testing/png_codec with a pure Rust backend using the png crate via CXX bindings: - Add testing/png_codec/png_codec_rust_ffi.rs implementing PNG encoding (BGR, BGRA, RGBA, Gray) and decoding via the png crate. - Add testing/png_codec/png_codec_rust.cpp implementing the png_codec C++ API using the Rust FFI. - Delete testing/png_codec/png_codec_skia.cpp, decoupling PNG testing code completely from Skia. - Update testing/png_codec/BUILD.gn to switch between the Rust PNG and libpng backends based on pdf_enable_rust_png. - Update testing/png_codec/DEPS. TAG=agy CONV=db6cb163-190b-47d4-bbdc-b519626ce208 Change-Id: I60a69517a58f9d6d53990daca30782d0c3b20004 Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/156030 Reviewed-by: Lei Zhang <thestig@chromium.org> Commit-Queue: Tom Sepez <tsepez@chromium.org>
diff --git a/testing/png_codec/BUILD.gn b/testing/png_codec/BUILD.gn index 2afa986..5b1d794 100644 --- a/testing/png_codec/BUILD.gn +++ b/testing/png_codec/BUILD.gn
@@ -5,14 +5,35 @@ import("../../pdfium.gni") import("../../testing/test.gni") +if (pdf_enable_rust_png) { + import("//build/rust/rust_static_library.gni") + + rust_static_library("png_codec_rust_ffi") { + testonly = true + crate_name = "png_codec_rust_ffi" + sources = [ "png_codec_rust_ffi.rs" ] + crate_root = "png_codec_rust_ffi.rs" + cxx_bindings = [ "png_codec_rust_ffi.rs" ] + allow_unsafe = true + deps = [ + "//build/rust:cxx_cppdeps", + "//third_party/rust/png/v0_18:lib", + ] + } +} + source_set("png_codec") { testonly = true sources = [ "png_codec.h" ] deps = [] - if (pdf_use_skia) { - sources += [ "png_codec_skia.cpp" ] - deps += [ "//skia" ] + if (pdf_enable_rust_png) { + sources += [ "png_codec_rust.cpp" ] + include_dirs = [ "$target_gen_dir/../.." ] + deps += [ + ":png_codec_rust_ffi", + "//build/rust:cxx_cppdeps", + ] } else { sources += [ "png_codec_libpng.cpp" ] deps += [
diff --git a/testing/png_codec/DEPS b/testing/png_codec/DEPS index a9cc1c6..17b552a 100644 --- a/testing/png_codec/DEPS +++ b/testing/png_codec/DEPS
@@ -1,5 +1,5 @@ include_rules = [ '+third_party/libpng', - '+third_party/skia/include', + '+third_party/rust/png', '+third_party/zlib', ]
diff --git a/testing/png_codec/png_codec_rust.cpp b/testing/png_codec/png_codec_rust.cpp new file mode 100644 index 0000000..8d5cf1f --- /dev/null +++ b/testing/png_codec/png_codec_rust.cpp
@@ -0,0 +1,89 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "testing/png_codec/png_codec.h" + +#include <stdint.h> + +#include <vector> + +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/span_util.h" +#include "testing/png_codec/png_codec_rust_ffi.rs.h" + +namespace png_codec { + +std::vector<uint8_t> Decode(pdfium::span<const uint8_t> input, + bool reverse_byte_order, + int* width, + int* height) { + rust::Slice<const uint8_t> src_slice(input); + int32_t w = 0; + int32_t h = 0; + rust::Vec<uint8_t> decoded = + rust_png::decode_png(src_slice, reverse_byte_order, w, h); + if (w <= 0 || h <= 0 || decoded.empty()) { + return {}; + } + *width = w; + *height = h; + return {decoded.begin(), decoded.end()}; +} + +std::vector<uint8_t> EncodeBGR(pdfium::span<const uint8_t> input, + int width, + int height, + int row_byte_width) { + rust::Slice<const uint8_t> src_slice(input); + rust::Vec<uint8_t> encoded = rust_png::encode_bgr( + src_slice, width, height, pdfium::checked_cast<size_t>(row_byte_width)); + if (encoded.empty()) { + return {}; + } + return {encoded.begin(), encoded.end()}; +} + +std::vector<uint8_t> EncodeRGBA(pdfium::span<const uint8_t> input, + int width, + int height, + int row_byte_width) { + rust::Slice<const uint8_t> src_slice(input); + rust::Vec<uint8_t> encoded = rust_png::encode_rgba( + src_slice, width, height, pdfium::checked_cast<size_t>(row_byte_width)); + if (encoded.empty()) { + return {}; + } + return {encoded.begin(), encoded.end()}; +} + +std::vector<uint8_t> EncodeBGRA(pdfium::span<const uint8_t> input, + int width, + int height, + int row_byte_width, + bool discard_transparency) { + rust::Slice<const uint8_t> src_slice(input); + rust::Vec<uint8_t> encoded = rust_png::encode_bgra( + src_slice, width, height, pdfium::checked_cast<size_t>(row_byte_width), + discard_transparency); + if (encoded.empty()) { + return {}; + } + return {encoded.begin(), encoded.end()}; +} + +std::vector<uint8_t> EncodeGray(pdfium::span<const uint8_t> input, + int width, + int height, + int row_byte_width) { + rust::Slice<const uint8_t> src_slice(input); + rust::Vec<uint8_t> encoded = rust_png::encode_gray( + src_slice, width, height, pdfium::checked_cast<size_t>(row_byte_width)); + if (encoded.empty()) { + return {}; + } + return {encoded.begin(), encoded.end()}; +} + +} // namespace png_codec
diff --git a/testing/png_codec/png_codec_rust_ffi.rs b/testing/png_codec/png_codec_rust_ffi.rs new file mode 100644 index 0000000..0dfa81d --- /dev/null +++ b/testing/png_codec/png_codec_rust_ffi.rs
@@ -0,0 +1,322 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::io::Cursor; + +#[cxx::bridge(namespace = "png_codec::rust_png")] +mod ffi { + extern "Rust" { + fn decode_png( + src: &[u8], + reverse_byte_order: bool, + width: &mut i32, + height: &mut i32, + ) -> Vec<u8>; + + fn encode_bgr(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8>; + + fn encode_rgba(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8>; + + fn encode_bgra( + input: &[u8], + width: i32, + height: i32, + row_byte_width: usize, + discard_transparency: bool, + ) -> Vec<u8>; + + fn encode_gray(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8>; + } +} + +fn decode_png( + src: &[u8], + reverse_byte_order: bool, + width_out: &mut i32, + height_out: &mut i32, +) -> Vec<u8> { + *width_out = 0; + *height_out = 0; + + let cursor = Cursor::new(src); + let mut decoder = png::Decoder::new(cursor); + decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); + let mut reader = match decoder.read_info() { + Ok(r) => r, + Err(_) => return Vec::new(), + }; + let buf_size = match reader.output_buffer_size() { + Some(s) => s, + None => return Vec::new(), + }; + let mut buf = vec![0; buf_size]; + let output_info = match reader.next_frame(&mut buf) { + Ok(info) => info, + Err(_) => return Vec::new(), + }; + let width = output_info.width as usize; + let height = output_info.height as usize; + if width == 0 || height == 0 { + return Vec::new(); + } + let line_size = output_info.line_size; + if line_size < width { + return Vec::new(); + } + let bytes_per_pixel = line_size / width; + if bytes_per_pixel == 0 || bytes_per_pixel > 4 { + return Vec::new(); + } + if let Some(total_src_bytes) = line_size.checked_mul(height) { + if total_src_bytes > buf.len() { + return Vec::new(); + } + } else { + return Vec::new(); + } + + let out_size = match width.checked_mul(height).and_then(|wh| wh.checked_mul(4)) { + Some(s) => s, + None => return Vec::new(), + }; + let mut out = vec![0u8; out_size]; + + // Standard default file gamma for sRGB (1.0 / 2.2). + const DEFAULT_FILE_GAMMA: f64 = 0.45455; + let file_gamma: f64 = if reader.info().srgb.is_some() { + DEFAULT_FILE_GAMMA + } else if let Some(gama) = reader.info().gama_chunk { + let val = gama.into_value() as f64; + if val > 0.0 { + val + } else { + DEFAULT_FILE_GAMMA + } + } else { + DEFAULT_FILE_GAMMA + }; + + const DISPLAY_GAMMA: f64 = 2.2; + let mut gamma_lut = [0u8; 256]; + let exponent = 1.0 / (file_gamma * DISPLAY_GAMMA); + if (exponent - 1.0).abs() < 1e-4 { + for (i, item) in gamma_lut.iter_mut().enumerate() { + *item = i as u8; + } + } else { + for (i, item) in gamma_lut.iter_mut().enumerate() { + let v = (i as f64) / 255.0; + *item = (v.powf(exponent) * 255.0).round().clamp(0.0, 255.0) as u8; + } + } + + for y in 0..height { + let src_start = y * line_size; + let src_row = &buf[src_start..src_start + line_size]; + let dst_start = y * width * 4; + let dst_slice = &mut out[dst_start..dst_start + width * 4]; + + for x in 0..width { + let (r, g, b, a) = match bytes_per_pixel { + 1 => { + let gray = gamma_lut[src_row[x] as usize]; + (gray, gray, gray, 255) + } + 2 => { + let gray = gamma_lut[src_row[x * 2] as usize]; + let alpha = src_row[x * 2 + 1]; + (gray, gray, gray, alpha) + } + 3 => { + let red = gamma_lut[src_row[x * 3] as usize]; + let green = gamma_lut[src_row[x * 3 + 1] as usize]; + let blue = gamma_lut[src_row[x * 3 + 2] as usize]; + (red, green, blue, 255) + } + 4 => { + let red = gamma_lut[src_row[x * 4] as usize]; + let green = gamma_lut[src_row[x * 4 + 1] as usize]; + let blue = gamma_lut[src_row[x * 4 + 2] as usize]; + let alpha = src_row[x * 4 + 3]; + (red, green, blue, alpha) + } + _ => return Vec::new(), + }; + + if reverse_byte_order { + dst_slice[x * 4] = b; + dst_slice[x * 4 + 1] = g; + dst_slice[x * 4 + 2] = r; + dst_slice[x * 4 + 3] = a; + } else { + dst_slice[x * 4] = r; + dst_slice[x * 4 + 1] = g; + dst_slice[x * 4 + 2] = b; + dst_slice[x * 4 + 3] = a; + } + } + } + + *width_out = width as i32; + *height_out = height as i32; + out +} + +fn write_png(data: &[u8], width: u32, height: u32, color_type: png::ColorType) -> Vec<u8> { + let mut out = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut out, width, height); + encoder.set_color(color_type); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = match encoder.write_header() { + Ok(w) => w, + Err(_) => return Vec::new(), + }; + if writer.write_image_data(data).is_err() { + return Vec::new(); + } + } + out +} + +fn encode_bgr(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8> { + if width <= 0 || height <= 0 { + return Vec::new(); + } + let w = width as usize; + let h = height as usize; + if row_byte_width < w * 3 { + return Vec::new(); + } + if let Some(total_input) = row_byte_width.checked_mul(h) { + if total_input > input.len() { + return Vec::new(); + } + } else { + return Vec::new(); + } + + let mut rgb = vec![0u8; w * h * 3]; + for y in 0..h { + let src_row = &input[y * row_byte_width..y * row_byte_width + w * 3]; + let dst_row = &mut rgb[y * w * 3..(y + 1) * w * 3]; + for x in 0..w { + dst_row[x * 3] = src_row[x * 3 + 2]; // R + dst_row[x * 3 + 1] = src_row[x * 3 + 1]; // G + dst_row[x * 3 + 2] = src_row[x * 3]; // B + } + } + write_png(&rgb, width as u32, height as u32, png::ColorType::Rgb) +} + +fn encode_rgba(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8> { + if width <= 0 || height <= 0 { + return Vec::new(); + } + let w = width as usize; + let h = height as usize; + if row_byte_width < w * 4 { + return Vec::new(); + } + if let Some(total_input) = row_byte_width.checked_mul(h) { + if total_input > input.len() { + return Vec::new(); + } + } else { + return Vec::new(); + } + + if row_byte_width == w * 4 { + return write_png(&input[..w * h * 4], width as u32, height as u32, png::ColorType::Rgba); + } + + let mut rgba = vec![0u8; w * h * 4]; + for y in 0..h { + let src_row = &input[y * row_byte_width..y * row_byte_width + w * 4]; + let dst_row = &mut rgba[y * w * 4..(y + 1) * w * 4]; + dst_row.copy_from_slice(src_row); + } + write_png(&rgba, width as u32, height as u32, png::ColorType::Rgba) +} + +fn encode_bgra( + input: &[u8], + width: i32, + height: i32, + row_byte_width: usize, + discard_transparency: bool, +) -> Vec<u8> { + if width <= 0 || height <= 0 { + return Vec::new(); + } + let w = width as usize; + let h = height as usize; + if row_byte_width < w * 4 { + return Vec::new(); + } + if let Some(total_input) = row_byte_width.checked_mul(h) { + if total_input > input.len() { + return Vec::new(); + } + } else { + return Vec::new(); + } + + if discard_transparency { + let mut rgb = vec![0u8; w * h * 3]; + for y in 0..h { + let src_row = &input[y * row_byte_width..y * row_byte_width + w * 4]; + let dst_row = &mut rgb[y * w * 3..(y + 1) * w * 3]; + for x in 0..w { + dst_row[x * 3] = src_row[x * 4 + 2]; // R + dst_row[x * 3 + 1] = src_row[x * 4 + 1]; // G + dst_row[x * 3 + 2] = src_row[x * 4]; // B + } + } + write_png(&rgb, width as u32, height as u32, png::ColorType::Rgb) + } else { + let mut rgba = vec![0u8; w * h * 4]; + for y in 0..h { + let src_row = &input[y * row_byte_width..y * row_byte_width + w * 4]; + let dst_row = &mut rgba[y * w * 4..(y + 1) * w * 4]; + for x in 0..w { + dst_row[x * 4] = src_row[x * 4 + 2]; // R + dst_row[x * 4 + 1] = src_row[x * 4 + 1]; // G + dst_row[x * 4 + 2] = src_row[x * 4]; // B + dst_row[x * 4 + 3] = src_row[x * 4 + 3]; // A + } + } + write_png(&rgba, width as u32, height as u32, png::ColorType::Rgba) + } +} + +fn encode_gray(input: &[u8], width: i32, height: i32, row_byte_width: usize) -> Vec<u8> { + if width <= 0 || height <= 0 { + return Vec::new(); + } + let w = width as usize; + let h = height as usize; + if row_byte_width < w { + return Vec::new(); + } + if let Some(total_input) = row_byte_width.checked_mul(h) { + if total_input > input.len() { + return Vec::new(); + } + } else { + return Vec::new(); + } + + if row_byte_width == w { + return write_png(&input[..w * h], width as u32, height as u32, png::ColorType::Grayscale); + } + + let mut gray = vec![0u8; w * h]; + for y in 0..h { + let src_row = &input[y * row_byte_width..y * row_byte_width + w]; + let dst_row = &mut gray[y * w..(y + 1) * w]; + dst_row.copy_from_slice(src_row); + } + write_png(&gray, width as u32, height as u32, png::ColorType::Grayscale) +}
diff --git a/testing/png_codec/png_codec_skia.cpp b/testing/png_codec/png_codec_skia.cpp deleted file mode 100644 index 2e1b7ed..0000000 --- a/testing/png_codec/png_codec_skia.cpp +++ /dev/null
@@ -1,186 +0,0 @@ -// Copyright 2025 The PDFium Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include <algorithm> -#include <memory> -#include <utility> -#include <vector> - -#include "core/fxcrt/check_op.h" -#include "core/fxcrt/fx_safe_types.h" -#include "core/fxcrt/numerics/checked_math.h" -#include "core/fxcrt/span.h" -#include "core/fxcrt/span_util.h" -#include "testing/png_codec/png_codec.h" -#include "third_party/skia/include/core/SkColorSpace.h" -#include "third_party/skia/include/core/SkColorType.h" -#include "third_party/skia/include/core/SkImageInfo.h" -#include "third_party/skia/include/core/SkStream.h" - -#ifdef PDF_ENABLE_RUST_PNG -#include "third_party/skia/include/codec/SkPngRustDecoder.h" -#include "third_party/skia/include/encode/SkPngRustEncoder.h" -#else -#include "third_party/skia/include/codec/SkPngDecoder.h" -#include "third_party/skia/include/encode/SkPngEncoder.h" -#endif - -namespace png_codec { - -namespace { - -std::vector<uint8_t> EncodeHelper(pdfium::span<const uint8_t> input, - SkColorType color, - SkAlphaType alpha, - int width, - int height, - size_t row_byte_width) { - SkImageInfo info = - SkImageInfo::Make(width, height, color, alpha, SkColorSpace::MakeSRGB()); - CHECK_NE(info.minRowBytes(), 0); // 0 means conversion problems. - CHECK_LE(info.minRowBytes(), row_byte_width); - CHECK_NE(info.computeMinByteSize(), 0); // 0 means conversion problems. - CHECK_LE(info.computeMinByteSize(), input.size()); - SkPixmap pixmap(info, input.data(), row_byte_width); - - SkDynamicMemoryWStream output; -#ifdef PDF_ENABLE_RUST_PNG - bool success = SkPngRustEncoder::Encode(&output, pixmap, {}); -#else - bool success = SkPngEncoder::Encode(&output, pixmap, {}); -#endif - if (!success) { - return {}; - } - return output.detachAsVector(); -} - -} // namespace - -std::vector<uint8_t> Decode(pdfium::span<const uint8_t> input, - bool reverse_byte_order, - int* width, - int* height) { - CHECK(width); - CHECK(height); - - auto stream = std::make_unique<SkMemoryStream>(input.data(), input.size(), - /*copyData=*/false); -#ifdef PDF_ENABLE_RUST_PNG - std::unique_ptr<SkCodec> codec = - SkPngRustDecoder::Decode(std::move(stream), nullptr); -#else - std::unique_ptr<SkCodec> codec = - SkPngDecoder::Decode(std::move(stream), nullptr); -#endif - if (!codec) { - return {}; - } - - SkColorType format = - reverse_byte_order ? kBGRA_8888_SkColorType : kRGBA_8888_SkColorType; - SkImageInfo info = codec->getInfo(); - info = info.makeColorType(format); - info = info.makeColorSpace(SkColorSpace::MakeSRGB()); - - std::vector<uint8_t> output; - output.resize(info.computeMinByteSize()); - - SkCodec::Result result = - codec->getPixels(info, output.data(), info.minRowBytes()); - if (result != SkCodec::kSuccess) { - return {}; - } - - *width = info.width(); - *height = info.height(); - return output; -} - -std::vector<uint8_t> EncodeBGR(pdfium::span<const uint8_t> bgr_input, - int width, - int height, - int row_byte_width) { - // Check inputs. Expected values are manually calculated (instead of using - // `SkImageInfo`'s `computeMinByteSize` and/or `minRowBytes`), because - // `SkColorType` doesn't cover a format with 3 bytes per pixel (bpp) - e.g. - // `kRGB_565_SkColorType` is 2 bpp and `kRGB_888x_SkColorType` is 4 bpp. - size_t row_byte_width_as_size_t = - pdfium::checked_cast<size_t>(row_byte_width); - FX_SAFE_SIZE_T expected_minimum_row_byte_width = 3; - expected_minimum_row_byte_width *= width; - CHECK_LE(expected_minimum_row_byte_width.ValueOrDie(), - row_byte_width_as_size_t); - - FX_SAFE_SIZE_T expected_minimum_input_size = row_byte_width_as_size_t; - expected_minimum_input_size *= height; - CHECK_LE(expected_minimum_input_size.ValueOrDie(), bgr_input.size()); - - // Convert `bgr_input` into `intermediate_bgra_buf` (because Skia doesn't - // allow encoding BGR pixels - see the comment at the top of the function - // that talks about limitations of `SkColorType`). - SkImageInfo intermediate_bgra_info = - SkImageInfo::Make(width, height, kBGRA_8888_SkColorType, - kOpaque_SkAlphaType, SkColorSpace::MakeSRGB()); - size_t intermediate_bgra_row_byte_width = - intermediate_bgra_info.minRowBytes(); - CHECK_NE(0, - intermediate_bgra_row_byte_width); // 0 means conversion problems. - std::vector<uint8_t> intermediate_bgra_buf; - intermediate_bgra_buf.resize(intermediate_bgra_info.computeMinByteSize()); - CHECK_NE(0, intermediate_bgra_buf.size()); // 0 means conversion problems. - { - pdfium::span<const uint8_t> src = bgr_input; - pdfium::span<uint8_t> dst = intermediate_bgra_buf; - size_t height_as_size_t = pdfium::checked_cast<size_t>(height); - size_t width_as_size_t = pdfium::checked_cast<size_t>(width); - for (size_t y = 0; y < height_as_size_t; y++) { - for (size_t x = 0; x < width_as_size_t; x++) { - // If `computeMinByteSize` didn't report an error (`0`), then integer - // overflow won't happen in the `x * N` expressions below. - pdfium::span<uint8_t> dst_pixel = dst.subspan(x * 4u).first(4u); - pdfium::span<const uint8_t> src_pixel = src.subspan(x * 3u).first(3u); - - fxcrt::spancpy(dst_pixel.first(3u), src_pixel); // Copy BGR channels. - dst_pixel[3] = 0xFF; // Set alpha channel to "opaque". - } - src = src.subspan(row_byte_width_as_size_t); - dst = dst.subspan(intermediate_bgra_row_byte_width); - } - } - - return EncodeHelper(intermediate_bgra_buf, kBGRA_8888_SkColorType, - kOpaque_SkAlphaType, width, height, - intermediate_bgra_row_byte_width); -} - -std::vector<uint8_t> EncodeRGBA(pdfium::span<const uint8_t> input, - int width, - int height, - int row_byte_width) { - return EncodeHelper(input, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType, - width, height, - pdfium::checked_cast<size_t>(row_byte_width)); -} - -std::vector<uint8_t> EncodeBGRA(pdfium::span<const uint8_t> input, - int width, - int height, - int row_byte_width, - bool discard_transparency) { - return EncodeHelper( - input, kBGRA_8888_SkColorType, - discard_transparency ? kOpaque_SkAlphaType : kUnpremul_SkAlphaType, width, - height, pdfium::checked_cast<size_t>(row_byte_width)); -} - -std::vector<uint8_t> EncodeGray(pdfium::span<const uint8_t> input, - int width, - int height, - int row_byte_width) { - return EncodeHelper(input, kGray_8_SkColorType, kOpaque_SkAlphaType, width, - height, pdfium::checked_cast<size_t>(row_byte_width)); -} - -} // namespace png_codec