我目前正在尝试重新压缩已经创建好的PDF文件,我在寻找一种方法来重新压缩文档中的图像以减小文件大小。
我一直在使用DataLogics PDE和iTextSharp库尝试进行操作,但是我找不到一种方法来对项目进行流重新压缩。
我考虑循环遍历xobjects并获取图像,然后将DPI降低到96或使用libjpeg C#实现来改变图像的质量,但是将其放回PDF流中似乎总是会导致内存损坏或其他问题。
非常感谢您能提供任何样例。
谢谢。
PdfReader.KillIndirect()
的方法,它可以删除对象;还有一个叫做PdfWriter.AddDirectImageSimple(iTextSharp.text.Image, PRIndirectReference)
的方法,你可以用它来替换已经删除的对象。var oldImage = PdfReader.GetPdfObject();
var newImage = YourImageCompressionFunction(oldImage);
PdfReader.KillIndirect(oldImage);
yourPdfWriter.AddDirectImageSimple(newImage, (PRIndirectReference)oldImage);
使用上面的方法,您的压缩算法实际上可以做两件事,物理缩小图像以及应用JPEG压缩。当您物理缩小图像并将其添加回去时,它将占用与原始图像相同的空间,但可用像素较少。这将使您获得您认为的DPI降低。JPEG压缩说明了一切。
以下是一个完整的工作中的C# 2010 WinForms应用程序,针对iTextSharp 5.1.1.0。它接收你桌面上现有的名为“LargeImage.jpg”的JPEG,并从中创建一个新的PDF。然后打开PDF,提取图像,将其物理缩小到原始大小的90%,应用85%的JPEG压缩并将其写回PDF。请参见代码中的注释以了解更多说明。该代码需要进行更多的空值/错误检查。还要查找NOTE
注释,您需要扩展以处理其他情况。using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
namespace WindowsFormsApplication1 {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
//Our working folder
string workingFolder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
//Large image to add to sample PDF
string largeImage = Path.Combine(workingFolder, "LargeImage.jpg");
//Name of large PDF to create
string largePDF = Path.Combine(workingFolder, "Large.pdf");
//Name of compressed PDF to create
string smallPDF = Path.Combine(workingFolder, "Small.pdf");
//Create a sample PDF containing our large image, for demo purposes only, nothing special here
using (FileStream fs = new FileStream(largePDF, FileMode.Create, FileAccess.Write, FileShare.None)) {
using (Document doc = new Document()) {
using (PdfWriter writer = PdfWriter.GetInstance(doc, fs)) {
doc.Open();
iTextSharp.text.Image importImage = iTextSharp.text.Image.GetInstance(largeImage);
doc.SetPageSize(new iTextSharp.text.Rectangle(0, 0, importImage.Width, importImage.Height));
doc.SetMargins(0, 0, 0, 0);
doc.NewPage();
doc.Add(importImage);
doc.Close();
}
}
}
//Now we're going to open the above PDF and compress things
//Bind a reader to our large PDF
PdfReader reader = new PdfReader(largePDF);
//Create our output PDF
using (FileStream fs = new FileStream(smallPDF, FileMode.Create, FileAccess.Write, FileShare.None)) {
//Bind a stamper to the file and our reader
using (PdfStamper stamper = new PdfStamper(reader, fs)) {
//NOTE: This code only deals with page 1, you'd want to loop more for your code
//Get page 1
PdfDictionary page = reader.GetPageN(1);
//Get the xobject structure
PdfDictionary resources = (PdfDictionary)PdfReader.GetPdfObject(page.Get(PdfName.RESOURCES));
PdfDictionary xobject = (PdfDictionary)PdfReader.GetPdfObject(resources.Get(PdfName.XOBJECT));
if (xobject != null) {
PdfObject obj;
//Loop through each key
foreach (PdfName name in xobject.Keys) {
obj = xobject.Get(name);
if (obj.IsIndirect()) {
//Get the current key as a PDF object
PdfDictionary imgObject = (PdfDictionary)PdfReader.GetPdfObject(obj);
//See if its an image
if (imgObject.Get(PdfName.SUBTYPE).Equals(PdfName.IMAGE)) {
//NOTE: There's a bunch of different types of filters, I'm only handing the simplest one here which is basically raw JPG, you'll have to research others
if (imgObject.Get(PdfName.FILTER).Equals(PdfName.DCTDECODE)) {
//Get the raw bytes of the current image
byte[] oldBytes = PdfReader.GetStreamBytesRaw((PRStream)imgObject);
//Will hold bytes of the compressed image later
byte[] newBytes;
//Wrap a stream around our original image
using (MemoryStream sourceMS = new MemoryStream(oldBytes)) {
//Convert the bytes into a .Net image
using (System.Drawing.Image oldImage = Bitmap.FromStream(sourceMS)) {
//Shrink the image to 90% of the original
using (System.Drawing.Image newImage = ShrinkImage(oldImage, 0.9f)) {
//Convert the image to bytes using JPG at 85%
newBytes = ConvertImageToBytes(newImage, 85);
}
}
}
//Create a new iTextSharp image from our bytes
iTextSharp.text.Image compressedImage = iTextSharp.text.Image.GetInstance(newBytes);
//Kill off the old image
PdfReader.KillIndirect(obj);
//Add our image in its place
stamper.Writer.AddDirectImageSimple(compressedImage, (PRIndirectReference)obj);
}
}
}
}
}
}
}
this.Close();
}
//Standard image save code from MSDN, returns a byte array
private static byte[] ConvertImageToBytes(System.Drawing.Image image, long compressionLevel) {
if (compressionLevel < 0) {
compressionLevel = 0;
} else if (compressionLevel > 100) {
compressionLevel = 100;
}
ImageCodecInfo jgpEncoder = GetEncoder(ImageFormat.Jpeg);
System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;
EncoderParameters myEncoderParameters = new EncoderParameters(1);
EncoderParameter myEncoderParameter = new EncoderParameter(myEncoder, compressionLevel);
myEncoderParameters.Param[0] = myEncoderParameter;
using (MemoryStream ms = new MemoryStream()) {
image.Save(ms, jgpEncoder, myEncoderParameters);
return ms.ToArray();
}
}
//standard code from MSDN
private static ImageCodecInfo GetEncoder(ImageFormat format) {
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs) {
if (codec.FormatID == format.Guid) {
return codec;
}
}
return null;
}
//Standard high quality thumbnail generation from http://weblogs.asp.net/gunnarpeipman/archive/2009/04/02/resizing-images-without-loss-of-quality.aspx
private static System.Drawing.Image ShrinkImage(System.Drawing.Image sourceImage, float scaleFactor) {
int newWidth = Convert.ToInt32(sourceImage.Width * scaleFactor);
int newHeight = Convert.ToInt32(sourceImage.Height * scaleFactor);
var thumbnailBitmap = new Bitmap(newWidth, newHeight);
using (Graphics g = Graphics.FromImage(thumbnailBitmap)) {
g.CompositingQuality = CompositingQuality.HighQuality;
g.SmoothingMode = SmoothingMode.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
System.Drawing.Rectangle imageRectangle = new System.Drawing.Rectangle(0, 0, newWidth, newHeight);
g.DrawImage(sourceImage, imageRectangle);
}
return thumbnailBitmap;
}
}
}
stream
和 endstream
之间有一个换行字节,您需要考虑到这一点,并在保存到文件之前从变量中删除它。不要对其使用字符串修剪函数,因为这会修剪掉您想要的一些字节并损坏图像。然后您只需要确保知道它是什么格式。嵌入式码流格式的JPEG2000没有完整的头文件,因此扩展名为 .jpc
,Jasper 可以处理此格式。JPEGs(DCTDecode)应该是正常的。 - Alasdair这里有一个关于如何在现有PDF中查找和替换图像的示例,作者是iText的创建者。实际上,这是从他的书中的一个小节摘录而来。由于它是使用Java编写的,因此这里提供一个简单的替代方案:
public void ReduceResolution(PdfReader reader, long quality) {
int n = reader.XrefSize;
for (int i = 0; i < n; i++) {
PdfObject obj = reader.GetPdfObject(i);
if (obj == null || !obj.IsStream()) {continue;}
PdfDictionary dict = (PdfDictionary)PdfReader.GetPdfObject(obj);
PdfName subType = (PdfName)PdfReader.GetPdfObject(
dict.Get(PdfName.SUBTYPE)
);
if (!PdfName.IMAGE.Equals(subType)) {continue;}
PRStream stream = (PRStream )obj;
try {
PdfImageObject image = new PdfImageObject(stream);
PdfName filter = (PdfName) image.Get(PdfName.FILTER);
if (
PdfName.JBIG2DECODE.Equals(filter)
|| PdfName.JPXDECODE.Equals(filter)
|| PdfName.CCITTFAXDECODE.Equals(filter)
|| PdfName.FLATEDECODE.Equals(filter)
) continue;
System.Drawing.Image img = image.GetDrawingImage();
if (img == null) continue;
var ll = image.GetImageBytesType();
int width = img.Width;
int height = img.Height;
using (System.Drawing.Bitmap dotnetImg =
new System.Drawing.Bitmap(img))
{
// set codec to jpeg type => jpeg index codec is "1"
System.Drawing.Imaging.ImageCodecInfo codec =
System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders()[1];
// set parameters for image quality
System.Drawing.Imaging.EncoderParameters eParams =
new System.Drawing.Imaging.EncoderParameters(1);
eParams.Param[0] =
new System.Drawing.Imaging.EncoderParameter(
System.Drawing.Imaging.Encoder.Quality, quality
);
using (MemoryStream msImg = new MemoryStream()) {
dotnetImg.Save(msImg, codec, eParams);
msImg.Position = 0;
stream.SetData(msImg.ToArray());
stream.SetData(
msImg.ToArray(), false, PRStream.BEST_COMPRESSION
);
stream.Put(PdfName.TYPE, PdfName.XOBJECT);
stream.Put(PdfName.SUBTYPE, PdfName.IMAGE);
stream.Put(PdfName.FILTER, filter);
stream.Put(PdfName.FILTER, PdfName.DCTDECODE);
stream.Put(PdfName.WIDTH, new PdfNumber(width));
stream.Put(PdfName.HEIGHT, new PdfNumber(height));
stream.Put(PdfName.BITSPERCOMPONENT, new PdfNumber(8));
stream.Put(PdfName.COLORSPACE, PdfName.DEVICERGB);
}
}
}
catch {
// throw;
// iText[Sharp] can't handle all image types...
}
finally {
// may or may not help
reader.RemoveUnusedObjects();
}
}
}
为了完整起见,由于您的问题特别涉及PDF压缩,以下是使用iTextSharp压缩PDF的方法:
PdfStamper stamper = new PdfStamper(
reader, YOUR-STREAM, PdfWriter.VERSION_1_5
);
stamper.Writer.CompressionLevel = 9;
int total = reader.NumberOfPages + 1;
for (int i = 1; i < total; i++) {
reader.SetPageContent(i, reader.GetPageContent(i));
}
stamper.SetFullCompression();
stamper.Close();
finally
块中调用RemoveUnusedObjects()
一样,它可能有助于减小文件大小,也可能没有。这取决于PDF是如何创建的。JBIG2DECODE
,所以@Alasdair的建议看起来不错。ReduceResolution()
方法后保存PDF:PdfReader
对象:PdfReader reader = new PdfReader(pdf);
b. 将 PdfReader
传递给上面的 ReduceResolution()
方法。
c. 将修改后的 PdfReader
传递给 PdfStamper
。以下是使用 MemoryStream
的一种方法:// Save altered PDF. then you can pass the btye array to a database, etc
using (MemoryStream ms = new MemoryStream()) {
using (PdfStamper stamper = new PdfStamper(reader, ms)) {
}
return ms.ToArray();
}
Stream
。例如,使用FileStream
并直接保存到磁盘上。PdfStamer(reader, new FileStream(@"C:\outputfile.pdf", FileMode.Create)
进行写入时,没有任何变化。也就是说,文件仍然是原来的样子。我做错了吗? - ZeeshanReduceResolution
的结果写入文件?这对我来说不起作用,或者我可能漏掉了什么。 - Zeeshan我不确定您是否考虑其他库,但是您可以轻松地使用Docotic.Pdf library(免责声明:我在这家公司工作)重新压缩现有图像。
以下是一些示例代码:
static void RecompressExistingImages(string fileName, string outputName)
{
using (PdfDocument doc = new PdfDocument(fileName))
{
foreach (PdfImage image in doc.Images)
image.RecompressWithGroup4Fax();
doc.Save(outputName);
}
}
还有RecompressWithFlate
、RecompressWithGroup3Fax
、RecompressWithJpeg
和Uncompress
方法。
如果需要,该库将把彩色图像转换为双色调图像。您可以指定deflate压缩级别、JPEG质量等。
我也要求您在使用@Alasdair建议的方法之前三思。如果您要处理不是由您创建的PDF文件,则任务比看起来复杂得多。
首先,有很多图像是由除JPXDecode
、JBIG2Decode
或DCTDecode
之外的编解码器压缩的。而且PDF还可以包含内联图像。
使用新版本标准(1.5或更高版本)保存的PDF文件可能包含交叉引用流。这意味着读取和更新此类文件比仅在文件末尾查找/更新一些数字更复杂。
因此,请使用PDF库。
压缩PDF的简单方法是使用gsdll32.dll(Ghostscript)和Cyotek.GhostScript.dll(包装器):
public static void CompressPDF(string sInFile, string sOutFile, int iResolution)
{
string[] arg = new string[]
{
"-sDEVICE=pdfwrite",
"-dNOPAUSE",
"-dSAFER",
"-dBATCH",
"-dCompatibilityLevel=1.5",
"-dDownsampleColorImages=true",
"-dDownsampleGrayImages=true",
"-dDownsampleMonoImages=true",
"-sPAPERSIZE=a4",
"-dPDFFitPage",
"-dDOINTERPOLATE",
"-dColorImageDownsampleThreshold=1.0",
"-dGrayImageDownsampleThreshold=1.0",
"-dMonoImageDownsampleThreshold=1.0",
"-dColorImageResolution=" + iResolution.ToString(),
"-dGrayImageResolution=" + iResolution.ToString(),
"-dMonoImageResolution=" + iResolution.ToString(),
"-sOutputFile=" + sOutFile,
sInFile
};
using(GhostScriptAPI api = new GhostScriptAPI())
{
api.Execute(arg);
}
}
MemoryStream
中,然后将其作为附加项或添加带有该Stream
的页面返回文档,对吗? - balexandre