1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id$ */
19 package org.apache.fop.pdf;
20 
21 import java.awt.Dimension;
22 import java.awt.Rectangle;
23 import java.awt.geom.Rectangle2D;
24 import java.io.ByteArrayInputStream;
25 import java.io.ByteArrayOutputStream;
26 import java.io.File;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.util.ArrayList;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33 
34 import javax.xml.transform.stream.StreamResult;
35 
36 import org.junit.Assert;
37 import org.junit.Test;
38 
39 import org.apache.fop.apps.FOUserAgent;
40 import org.apache.fop.apps.FopFactory;
41 import org.apache.fop.fonts.FontInfo;
42 import org.apache.fop.render.intermediate.IFContext;
43 import org.apache.fop.render.pdf.PDFContentGenerator;
44 import org.apache.fop.render.pdf.PDFDocumentHandler;
45 import org.apache.fop.render.pdf.PDFPainter;
46 
47 public class PDFLinearizationTestCase {
48     private int objectLeast;
49     private int[] objects;
50 
51     @Test
testPDF()52     public void testPDF() throws IOException {
53         PDFDocument doc = new PDFDocument("");
54         doc.setLinearizationEnabled(true);
55         PDFResources resources = new PDFResources(doc);
56         PDFResourceContext context = new PDFResourceContext(resources);
57         ByteArrayOutputStream out = new ByteArrayOutputStream();
58         PDFContentGenerator gen = null;
59         for (int i = 0; i < 2; i++) {
60             gen = new PDFContentGenerator(doc, out, context);
61             Rectangle2D.Float f = new Rectangle2D.Float();
62             PDFPage page = new PDFPage(resources, i, f, f, f, f);
63             doc.registerObject(page);
64             doc.registerObject(gen.getStream());
65             page.setContents(new PDFReference(gen.getStream()));
66         }
67         gen.flushPDFDoc();
68         byte[] data = out.toByteArray();
69         checkPDF(data);
70     }
71 
72     @Test
testImage()73     public void testImage() throws Exception {
74         String fopxconf = "<fop version=\"1.0\"><renderers>"
75                 + "<renderer mime=\"application/pdf\">"
76                 + "<linearization>true</linearization>"
77                 + "</renderer></renderers></fop>";
78         FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI(),
79                 new ByteArrayInputStream(fopxconf.getBytes()));
80         FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
81         IFContext ifContext = new IFContext(foUserAgent);
82         PDFDocumentHandler documentHandler = new PDFDocumentHandler(ifContext);
83         documentHandler.getConfigurator().configure(documentHandler);
84         ByteArrayOutputStream out = new ByteArrayOutputStream();
85         documentHandler.setFontInfo(new FontInfo());
86         documentHandler.setResult(new StreamResult(out));
87         documentHandler.startDocument();
88         documentHandler.startPage(0, "", "", new Dimension());
89         PDFPainter pdfPainter = new PDFPainter(documentHandler, null);
90         pdfPainter.drawImage("test/resources/fop/svg/logo.jpg", new Rectangle());
91         documentHandler.endPage();
92         Assert.assertFalse(out.toString().contains("/Subtype /Image"));
93         documentHandler.endDocument();
94         Assert.assertTrue(out.toString().contains("/Subtype /Image"));
95     }
96 
checkPDF(byte[] data)97     private void checkPDF(byte[] data) throws IOException {
98         checkHintTable(data);
99         InputStream is = new ByteArrayInputStream(data);
100         Map<String, StringBuilder> objs = readObjs(is);
101 
102         List<String> keys = new ArrayList<String>(objs.keySet());
103         int start = keys.indexOf("1 0 obj");
104         Assert.assertTrue(start > 1);
105         int j = 1;
106         for (int i = start; i < keys.size(); i++) {
107             Assert.assertEquals(keys.get(i), j + " 0 obj");
108             j++;
109         }
110         for (int i = 0; i < start; i++) {
111             Assert.assertEquals(keys.get(i), j + " 0 obj");
112             j++;
113         }
114 
115         checkFirstObj(data);
116         checkTrailer(data);
117 
118         String firstObj = objs.values().iterator().next().toString().replace("\n", "");
119         Assert.assertTrue(firstObj.startsWith("<<  /Linearized 1  /L " + data.length));
120         Assert.assertTrue(firstObj.endsWith("startxref0%%EOF"));
121         int pageObjNumber = getValue("/O", firstObj);
122         Assert.assertTrue(objs.get(pageObjNumber + " 0 obj").toString().contains("/Type /Page"));
123         Assert.assertTrue(objs.get("5 0 obj").toString().contains("/Type /Pages"));
124 
125         int total = 0;
126         for (int i : objects) {
127             total += i;
128         }
129         Assert.assertEquals(total, objs.size() - 6);
130     }
131 
checkFirstObj(byte[] data)132     private void checkFirstObj(byte[] data) throws IOException {
133         int firstObjPos = getValue("/E", getFirstObj(data));
134         InputStream is = new ByteArrayInputStream(data);
135         Assert.assertEquals(is.skip(firstObjPos), firstObjPos);
136         byte[] obj = new byte[10];
137         Assert.assertEquals(is.read(obj), obj.length);
138         Assert.assertTrue(new String(obj).startsWith("1 0 obj"));
139     }
140 
checkTrailer(byte[] data)141     private void checkTrailer(byte[] data) throws IOException {
142         int trailerPos = getValue("/T", getFirstObj(data));
143         InputStream is = new ByteArrayInputStream(data);
144         Assert.assertEquals(is.skip(trailerPos), trailerPos);
145         byte[] obj = new byte[20];
146         Assert.assertEquals(is.read(obj), obj.length);
147         Assert.assertTrue(new String(obj).startsWith("0000000000 65535 f"));
148     }
149 
getValue(String name, String firstObj)150     private int getValue(String name, String firstObj) throws IOException {
151         String[] split = firstObj.split(" ");
152         for (int i = 0; i < split.length; i++) {
153             if (split[i].equals(name)) {
154                 return Integer.valueOf(split[i + 1].replace(">>", ""));
155             }
156         }
157         throw new IOException(name + " not found " + firstObj);
158     }
159 
getArrayValue(String name, String firstObj)160     private int[] getArrayValue(String name, String firstObj) throws IOException {
161         String[] split = firstObj.split(" ");
162         for (int i = 0; i < split.length; i++) {
163             if (split[i].equals(name)) {
164                 int[] v = new int[2];
165                 v[0] = Integer.valueOf(split[i + 1].replace("[", ""));
166                 v[1] = Integer.valueOf(split[i + 2].replace("]", ""));
167                 return v;
168             }
169         }
170         throw new IOException(name + " not found " + firstObj);
171     }
172 
getFirstObj(byte[] out)173     private String getFirstObj(byte[] out) throws IOException {
174         InputStream data = new ByteArrayInputStream(out);
175         Map<String, StringBuilder> objs = readObjs(data);
176         return objs.values().iterator().next().toString().replace("\n", "");
177     }
178 
checkHintTable(byte[] out)179     private void checkHintTable(byte[] out) throws IOException {
180         String firstObj = getFirstObj(out);
181         int hintPos = getArrayValue("/H", firstObj)[0];
182         int hintLength = getArrayValue("/H", firstObj)[1];
183 
184         InputStream data = new ByteArrayInputStream(out);
185         Assert.assertEquals(data.skip(hintPos), hintPos);
186 
187         byte[] hintTable = new byte[hintLength];
188         Assert.assertEquals(data.read(hintTable), hintLength);
189         String hintTableStr = new String(hintTable);
190 
191         Assert.assertTrue(hintTableStr.contains("/S "));
192         Assert.assertTrue(hintTableStr.contains("/C "));
193         Assert.assertTrue(hintTableStr.contains("/E "));
194         Assert.assertTrue(hintTableStr.contains("/L "));
195         Assert.assertTrue(hintTableStr.contains("/V "));
196         Assert.assertTrue(hintTableStr.contains("/O "));
197         Assert.assertTrue(hintTableStr.contains("/I "));
198         Assert.assertTrue(hintTableStr.contains("/Length "));
199         Assert.assertTrue(hintTableStr.contains("stream"));
200         Assert.assertTrue(hintTableStr.contains("endstream"));
201         Assert.assertTrue(hintTableStr.endsWith("endobj\n"));
202 
203         data = new ByteArrayInputStream(hintTable);
204         readStart(data);
205         int pages = getValue("/N", firstObj);
206         readObjectsTable(data, pages);
207         readSharedObjectsTable(data);
208         Assert.assertEquals(objectLeast, 1);
209     }
210 
readObjectsTable(InputStream data, int pages)211     private void readObjectsTable(InputStream data, int pages)
212             throws IOException {
213         objectLeast = read32(data);
214         read32(data);
215         int bitsDiffObjects = read16(data);
216         read32(data);
217         int bitsDiffPageLength = read16(data);
218         read32(data);
219         read16(data);
220         read32(data);
221         read16(data);
222         read16(data);
223         read16(data);
224         read16(data);
225         read16(data);
226 
227         objects = new int[pages];
228         for (int i = 0; i < pages; i++) {
229             objects[i] = objectLeast + readBits(bitsDiffObjects, data);
230         }
231         for (int i = 0; i < pages; i++) {
232             readBits(bitsDiffPageLength, data);
233         }
234         for (int i = 0; i < pages; i++) {
235             readBits(32, data);
236         }
237     }
238 
readSharedObjectsTable(InputStream str)239     private void readSharedObjectsTable(InputStream str) throws IOException {
240         readBits(32, str);
241         readBits(32, str);
242         readBits(32, str);
243         int sharedGroups = readBits(32, str);
244         readBits(16, str);
245         readBits(32, str);
246         int bitsDiffGroupLength = readBits(16, str);
247         for (int i = 0; i < sharedGroups; i++) {
248             readBits(bitsDiffGroupLength, str);
249         }
250     }
251 
readBits(int bits, InputStream data)252     private int readBits(int bits, InputStream data) throws IOException {
253         if (bits == 32) {
254             return read32(data);
255         }
256         if (bits == 16) {
257             return read16(data);
258         }
259         throw new IOException("Wrong bits");
260     }
261 
read32(InputStream data)262     private int read32(InputStream data) throws IOException {
263         int ch1 = data.read();
264         int ch2 = data.read();
265         int ch3 = data.read();
266         int ch4 = data.read();
267         return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4));
268     }
269 
read16(InputStream data)270     private int read16(InputStream data) throws IOException {
271         int ch1 = data.read();
272         int ch2 = data.read();
273         return (ch1 << 8) + (ch2);
274     }
275 
readStart(InputStream inputStream)276     private void readStart(InputStream inputStream) throws IOException {
277         StringBuilder sb = new StringBuilder();
278         while (inputStream.available() > 0) {
279             int data = inputStream.read();
280             if (data == '\n') {
281                 if (sb.toString().equals("stream")) {
282                     return;
283                 }
284                 sb.setLength(0);
285             } else {
286                 sb.append((char)data);
287             }
288         }
289     }
290 
readObjs(InputStream inputStream)291     public static Map<String, StringBuilder> readObjs(InputStream inputStream) throws IOException {
292         Map<String, StringBuilder> objs = new LinkedHashMap<String, StringBuilder>();
293         StringBuilder sb = new StringBuilder();
294         String key = null;
295         while (inputStream.available() > 0) {
296             int data = inputStream.read();
297             if (data == '\n') {
298                 if (sb.toString().endsWith(" 0 obj")) {
299                     key = sb.toString().trim();
300                     objs.put(key, new StringBuilder());
301                 } else if (key != null) {
302                     objs.get(key).append(sb).append("\n");
303                 }
304                 sb.setLength(0);
305             } else {
306                 sb.append((char)data);
307             }
308         }
309         return objs;
310     }
311 }
312