001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.commons.pdf;
020
021import java.awt.Dimension;
022import java.awt.Insets;
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.OutputStream;
029import java.io.StringReader;
030import java.net.MalformedURLException;
031import java.net.URL;
032
033import lucee.commons.io.CharsetUtil;
034import lucee.commons.io.IOUtil;
035import lucee.commons.io.SystemUtil;
036import lucee.commons.io.res.ContentType;
037import lucee.commons.io.res.Resource;
038import lucee.commons.io.res.util.ResourceUtil;
039import lucee.commons.lang.ExceptionUtil;
040import lucee.commons.lang.HTMLEntities;
041import lucee.commons.lang.StringUtil;
042import lucee.commons.net.HTTPUtil;
043import lucee.commons.net.http.HTTPEngine;
044import lucee.commons.net.http.HTTPResponse;
045import lucee.runtime.Info;
046import lucee.runtime.PageContext;
047import lucee.runtime.PageContextImpl;
048import lucee.runtime.config.ConfigWeb;
049import lucee.runtime.exp.ExpressionException;
050import lucee.runtime.exp.PageException;
051import lucee.runtime.functions.system.ContractPath;
052import lucee.runtime.functions.system.GetDirectoryFromPath;
053import lucee.runtime.net.http.ReqRspUtil;
054import lucee.runtime.net.proxy.ProxyData;
055import lucee.runtime.net.proxy.ProxyDataImpl;
056import lucee.runtime.op.Caster;
057import lucee.runtime.text.xml.XMLCaster;
058import lucee.runtime.text.xml.XMLUtil;
059import lucee.runtime.type.util.ListUtil;
060import lucee.runtime.util.URLResolver;
061
062import org.w3c.dom.Document;
063import org.w3c.dom.Element;
064import org.xml.sax.InputSource;
065import org.xml.sax.SAXException;
066
067public final class PDFDocument {
068
069        // PageType
070    public static final Dimension PAGETYPE_ISOB5 = new Dimension(501, 709);
071    public static final Dimension PAGETYPE_ISOB4 = new Dimension(709, 1002);
072    public static final Dimension PAGETYPE_ISOB3 = new Dimension(1002, 1418);
073    public static final Dimension PAGETYPE_ISOB2 = new Dimension(1418, 2004);
074    public static final Dimension PAGETYPE_ISOB1 = new Dimension(2004, 2836);
075    public static final Dimension PAGETYPE_ISOB0 = new Dimension(2836, 4008);
076    public static final Dimension PAGETYPE_HALFLETTER = new Dimension(396, 612);
077    public static final Dimension PAGETYPE_LETTER = new Dimension(612, 792);
078    public static final Dimension PAGETYPE_TABLOID = new Dimension(792, 1224);
079    public static final Dimension PAGETYPE_LEDGER = new Dimension(1224, 792);
080    public static final Dimension PAGETYPE_NOTE = new Dimension(540, 720);
081    public static final Dimension PAGETYPE_LEGAL = new Dimension(612, 1008);
082        
083    public static final Dimension PAGETYPE_A10 = new Dimension(74, 105);
084    public static final Dimension PAGETYPE_A9 = new Dimension(105, 148);
085    public static final Dimension PAGETYPE_A8 = new Dimension(148, 210);
086    public static final Dimension PAGETYPE_A7 = new Dimension(210, 297);
087    public static final Dimension PAGETYPE_A6 = new Dimension(297, 421);
088    public static final Dimension PAGETYPE_A5 = new Dimension(421, 595);
089    public static final Dimension PAGETYPE_A4 = new Dimension(595, 842);
090    public static final Dimension PAGETYPE_A3 = new Dimension(842, 1190);
091    public static final Dimension PAGETYPE_A2 = new Dimension(1190, 1684);
092    public static final Dimension PAGETYPE_A1 = new Dimension(1684, 2384);
093    public static final Dimension PAGETYPE_A0 = new Dimension(2384, 3370);
094        
095        
096        public static final Dimension PAGETYPE_B4=new Dimension(708,1000);
097        public static final Dimension PAGETYPE_B5=new Dimension(499,708);
098        public static final Dimension PAGETYPE_B4_JIS=new Dimension(728,1031);
099        public static final Dimension PAGETYPE_B5_JIS=new Dimension(516,728);
100        public static final Dimension PAGETYPE_CUSTOM=new Dimension(1,1);
101                        
102        // encryption
103        public static final int ENC_NONE=0;
104        public static final int ENC_40BIT=1;
105        public static final int ENC_128BIT=2;
106        
107        //      fontembed 
108        public static final int FONT_EMBED_NO=0;
109        public static final int FONT_EMBED_YES=1;
110        public static final int FONT_EMBED_SELECCTIVE=FONT_EMBED_YES;
111
112        // unit
113        public static final double UNIT_FACTOR_CM=85d/3d;// =28.333333333333333333333333333333333333333333;
114        public static final double UNIT_FACTOR_IN=UNIT_FACTOR_CM*2.54;
115        public static final double UNIT_FACTOR_POINT=1;
116                
117        // margin init
118        private static final int MARGIN_INIT=36;
119
120        // mimetype
121        private static final int MIMETYPE_TEXT_HTML = 0;
122        private static final int MIMETYPE_TEXT = 1;
123        private static final int MIMETYPE_IMAGE = 2;  
124        private static final int MIMETYPE_APPLICATION = 3;
125        private static final int MIMETYPE_OTHER = -1;
126        private static final String USER_AGENT = "Lucee "+Info.getVersionAsString()+" "+Info.getStateAsString();
127        
128                
129        private double margintop=-1;
130        private double marginbottom=-1;
131        private double marginleft=-1;
132        private double marginright=-1;
133
134        private int mimetype=MIMETYPE_TEXT_HTML;
135        private String strMimetype=null;
136        private String strCharset=null;
137
138        private boolean backgroundvisible;
139        private boolean fontembed=true;
140        private PDFPageMark header;
141        private PDFPageMark footer;
142        
143        private String proxyserver;
144        private int proxyport=80;
145        private String proxyuser=null;
146        private String proxypassword="";
147
148        private String src=null;
149        private Resource srcfile=null;
150        private String body;
151        //private boolean isEvaluation;
152        private String name;
153        private String authUser;
154        private String authPassword;
155        private String userAgent=USER_AGENT;
156        private boolean localUrl;
157        private boolean bookmark; 
158        private boolean htmlBookmark;
159        
160        
161        
162        public PDFDocument(){
163                //this.isEvaluation=isEvaluation;
164            
165        }
166
167
168        public void setHeader(PDFPageMark header) {
169                this.header=header;
170        }
171
172        public void setFooter(PDFPageMark footer) {
173                this.footer=footer;
174        }
175        
176
177        /**
178         * @param marginbottom the marginbottom to set
179         */
180        public void setMarginbottom(double marginbottom) {
181                this.marginbottom = marginbottom;
182        }
183
184        /**
185         * @param marginleft the marginleft to set
186         */
187        public void setMarginleft(double marginleft) {
188                this.marginleft = marginleft;
189        }
190
191        /**
192         * @param marginright the marginright to set
193         */
194        public void setMarginright(double marginright) {
195                this.marginright = marginright;
196        }
197
198        /**
199         * @param margintop the margintop to set
200         */
201        public void setMargintop(double margintop) {
202                this.margintop = margintop;
203        }
204        
205        /**
206         * @param strMimetype the mimetype to set
207         */
208        public void setMimetype(String strMimetype) {
209                strMimetype = strMimetype.toLowerCase().trim();
210                this.strMimetype=strMimetype;
211                // mimetype
212                if(strMimetype.startsWith("text/html"))                 mimetype=MIMETYPE_TEXT_HTML;
213                else if(strMimetype.startsWith("text/"))                mimetype=MIMETYPE_TEXT;
214                else if(strMimetype.startsWith("image/"))               mimetype=MIMETYPE_IMAGE;
215                else if(strMimetype.startsWith("application/"))         mimetype=MIMETYPE_APPLICATION;
216                else mimetype=MIMETYPE_OTHER;
217                
218                // charset
219                String[] arr = ListUtil.listToStringArray(strMimetype, ';');
220                if(arr.length>=2) {
221                        this.strMimetype=arr[0].trim();
222                        for(int i=1;i<arr.length;i++) {
223                                String[] item = ListUtil.listToStringArray(arr[i], '=');
224                                if(item.length==1) {
225                                        strCharset=item[0].trim();
226                                        break;
227                                }
228                                else if(item.length==2 && item[0].trim().equals("charset")) {
229                                        strCharset=item[1].trim();
230                                        break;
231                                }
232                        }
233                }
234        }
235        
236        /** set the value proxyserver
237        *  Host name or IP address of a proxy server.
238        * @param proxyserver value to set
239        **/
240        public void setProxyserver(String proxyserver)  {
241                this.proxyserver=proxyserver;
242        }
243        
244        /** set the value proxyport
245        *  The port number on the proxy server from which the object is requested. Default is 80. When 
246        *       used with resolveURL, the URLs of retrieved documents that specify a port number are automatically 
247        *       resolved to preserve links in the retrieved document.
248        * @param proxyport value to set
249        **/
250        public void setProxyport(int proxyport) {
251                this.proxyport=proxyport;
252        }
253
254        /** set the value username
255        *  When required by a proxy server, a valid username.
256        * @param proxyuser value to set
257        **/
258        public void setProxyuser(String proxyuser)      {
259                this.proxyuser=proxyuser;
260        }
261
262        /** set the value password
263        *  When required by a proxy server, a valid password.
264        * @param proxypassword value to set
265        **/
266        public void setProxypassword(String proxypassword)      {
267                this.proxypassword=proxypassword;
268        }
269
270        /**
271         * @param src
272         * @throws PDFException
273         */
274        public void setSrc(String src) throws PDFException {
275                if(srcfile!=null) throw new PDFException("You cannot specify both the src and srcfile attributes");
276                this.src = src;
277        }
278        
279
280        /**
281         * @param srcfile the srcfile to set
282         * @throws PDFException 
283         */
284        public void setSrcfile(Resource srcfile) throws PDFException {
285                if(src!=null) throw new PDFException("You cannot specify both the src and srcfile attributes");
286                this.srcfile=srcfile;
287        }
288
289        public void setBody(String body) {
290                this.body=body;
291        }
292
293        public byte[] render(Dimension dimension,double unitFactor, PageContext pc,boolean generateOutlines) throws PageException, IOException {
294                ConfigWeb config = pc.getConfig();
295                PDF pd4ml = new PDF(config);
296                pd4ml.generateOutlines(generateOutlines);
297                pd4ml.enableTableBreaks(true);
298                pd4ml.interpolateImages(true);
299                // MUSTMUST DO NOT ENABLE, why this was disabled
300                pd4ml.adjustHtmlWidth();
301                
302                //check size
303                int mTop =      toPoint(margintop,unitFactor);
304                int mLeft = toPoint(marginleft,unitFactor);
305                int mBottom=toPoint(marginbottom,unitFactor);
306                int mRight=toPoint(marginright,unitFactor);
307                if((mLeft+mRight)>dimension.getWidth())
308                        throw new ExpressionException("current document width ("+Caster.toString(dimension.getWidth())+" point) is smaller that specified horizontal margin  ("+Caster.toString(mLeft+mRight)+" point).",
309                                        "1 in = "+Math.round(1*UNIT_FACTOR_IN)+" point and 1 cm = "+Math.round(1*UNIT_FACTOR_CM)+" point");
310                if((mTop+mBottom)>dimension.getHeight())
311                        throw new ExpressionException("current document height ("+Caster.toString(dimension.getHeight())+" point) is smaller that specified vertical margin  ("+Caster.toString(mTop+mBottom)+" point).",
312                                        "1 in = "+Math.round(1*UNIT_FACTOR_IN)+" point and 1 cm = "+Math.round(1*UNIT_FACTOR_CM)+" point");
313                
314                // Size
315                pd4ml.setPageInsets(new Insets(mTop,mLeft,mBottom,mRight));
316                pd4ml.setPageSize(dimension);
317                
318                // header
319                if(header!=null) pd4ml.setPageHeader(header);
320                // footer
321                if(footer!=null) pd4ml.setPageFooter(footer);
322                
323                // content
324                ByteArrayOutputStream baos=new ByteArrayOutputStream();
325                try {
326                        content(pd4ml,pc,baos);
327                        
328                }
329                finally {
330                        IOUtil.closeEL(baos);
331                }
332                return baos.toByteArray();
333        }
334
335        private void content(PDF pd4ml, PageContext pc, OutputStream os) throws PageException, IOException {
336                ConfigWeb config = pc.getConfig();
337                pd4ml.useTTF("java:fonts", fontembed);
338                
339                // body
340        if(!StringUtil.isEmpty(body,true)) {
341                // optimize html
342                URL base = getBase(pc);
343                try {
344                        body=beautifyHTML(new InputSource(new StringReader(body)),base);
345                        }catch (Throwable t) {
346                ExceptionUtil.rethrowIfNecessary(t);
347            }
348                        
349                pd4ml.render(body, os,base);
350                        
351        }
352        // srcfile
353        else if(srcfile!=null) {
354                if(StringUtil.isEmpty(strCharset))strCharset=((PageContextImpl)pc).getResourceCharset().name();
355                
356                        // mimetype
357                        if(StringUtil.isEmpty(strMimetype)) {
358                                String mt = ResourceUtil.getMimeType(srcfile,null);
359                                if(mt!=null) setMimetype(mt);
360                        }
361                        InputStream is = srcfile.getInputStream();
362                try {
363                        
364                        URL base = new URL("file://"+srcfile);
365                        if(!localUrl){
366                                //PageContext pc = Thread LocalPageContext.get();
367                                
368                                        String abs = srcfile.getAbsolutePath();
369                                        String contract = ContractPath.call(pc, abs);
370                                        if(!abs.equals(contract)) {
371                                                base=HTTPUtil.toURL(ReqRspUtil.getDomain(pc.getHttpServletRequest())+contract,true);
372                                        }
373
374                        }
375                        
376                        //URL base = localUrl?new URL("file://"+srcfile):getBase();
377                        render(pd4ml, is,os,base);
378                        } 
379                catch (Throwable t) {
380                ExceptionUtil.rethrowIfNecessary(t);
381            }
382                finally {
383                        IOUtil.closeEL(is);
384                }
385        }
386        // src
387        else if(src!=null) {
388                if(StringUtil.isEmpty(strCharset))strCharset="iso-8859-1";
389                URL url = HTTPUtil.toURL(src,true);
390                        
391                        // set Proxy
392                        if(StringUtil.isEmpty(proxyserver) && config.isProxyEnableFor(url.getHost())) {
393                                ProxyData pd = config.getProxyData();
394                                proxyserver=pd==null?null:pd.getServer();
395                                proxyport=pd==null?0:pd.getPort();
396                                proxyuser=pd==null?null:pd.getUsername();
397                                proxypassword=pd==null?null:pd.getPassword();
398                        }
399                        
400                        HTTPResponse method = HTTPEngine.get(url, authUser, authPassword, -1,HTTPEngine.MAX_REDIRECT, null, userAgent,
401                                        ProxyDataImpl.getInstance(proxyserver, proxyport, proxyuser, proxypassword),null);
402                        
403                        // mimetype
404                        if(StringUtil.isEmpty(strMimetype)) {
405                                ContentType ct = method.getContentType();
406                                if(ct!=null)
407                                        setMimetype(ct.toString());
408                                
409                        }
410                        InputStream is = new ByteArrayInputStream(method.getContentAsByteArray());
411                        try {
412                                
413                                render(pd4ml, is, os,url);
414                        }
415                        finally {
416                                IOUtil.closeEL(is);
417                        }
418        }
419        else {
420                pd4ml.render("<html><body> </body></html>", os,null);
421        }
422        }
423
424        private static String beautifyHTML(InputSource is,URL base) throws ExpressionException, SAXException, IOException {
425                Document xml = XMLUtil.parse(is,null,true);
426                patchPD4MLProblems(xml);
427                
428                if(base!=null)URLResolver.getInstance().transform(xml, base);
429                String html = XMLCaster.toHTML(xml);
430                return html;
431        }
432
433        private static void patchPD4MLProblems(Document xml) {
434                Element b = XMLUtil.getChildWithName("body", xml.getDocumentElement());
435                if(!b.hasChildNodes()){
436                        b.appendChild(xml.createTextNode(" "));
437                }
438        }
439
440
441        private static URL getBase(PageContext pc) throws MalformedURLException {
442                //PageContext pc = Thread LocalPageContext.get();
443                if(pc==null)return null;
444                
445                String userAgent = pc.getHttpServletRequest().getHeader("User-Agent");
446                // bug in pd4ml-> html badse definition create a call
447                if(!StringUtil.isEmpty(userAgent) && userAgent.startsWith("Java"))return null;
448                
449                return HTTPUtil.toURL(GetDirectoryFromPath.call(pc, ReqRspUtil.getRequestURL(pc.getHttpServletRequest(), false)),true);
450        }
451
452
453        private void render(PDF pd4ml, InputStream is,OutputStream os, URL base) throws IOException, PageException {
454                try {
455                        
456                        // text/html
457                        if(mimetype==MIMETYPE_TEXT_HTML) {
458                                body="";
459                                
460                                try {
461                                        InputSource input = new InputSource(IOUtil.getReader(is,CharsetUtil.toCharset(strCharset)));
462                                        body=beautifyHTML(input,base);
463                                } 
464                                catch (Throwable t) {
465                        ExceptionUtil.rethrowIfNecessary(t);
466                    }
467                                //else if(body==null)body =IOUtil.toString(is,strCharset); 
468                                pd4ml.render(body, os,base);
469                        }
470                        // text
471                        else if(mimetype==MIMETYPE_TEXT) {
472                                body =IOUtil.toString(is,strCharset); 
473                                body="<html><body><pre>"+HTMLEntities.escapeHTML(body)+"</pre></body></html>";
474                                pd4ml.render(body, os,null);
475                        }
476                        // image
477                        else if(mimetype==MIMETYPE_IMAGE) {
478                                Resource tmpDir= SystemUtil.getTempDirectory();
479                                Resource tmp = tmpDir.getRealResource(this+"-"+Math.random());
480                                IOUtil.copy(is, tmp,true);
481                                body="<html><body><img src=\"file://"+tmp+"\"></body></html>";
482                                try {
483                                        pd4ml.render(body, os,null);
484                                }
485                                finally {
486                                        tmp.delete();
487                                }       
488                        }
489                        // Application
490                        else if(mimetype==MIMETYPE_APPLICATION && "application/pdf".equals(strMimetype)) {
491                                IOUtil.copy(is, os,true,true);
492                        }
493                        else pd4ml.render(new InputStreamReader(is), os);
494                }
495                finally {
496                        IOUtil.closeEL(is,os);
497                }
498        }
499
500        public static int toPoint(double value,double unitFactor) {
501                if(value<0) return MARGIN_INIT;
502                return (int)Math.round(value*unitFactor);
503                //return r;
504        }
505
506        public PDFPageMark getHeader() {
507                return header;
508        }
509        public PDFPageMark getFooter() {
510                return footer;
511        }
512
513        public void setFontembed(int fontembed) {
514                this.fontembed=fontembed!=FONT_EMBED_NO;
515        }
516
517
518        /**
519         * @return the name
520         */
521        public String getName() {
522                return name;
523        }
524
525
526        /**
527         * @param name the name to set
528         */
529        public void setName(String name) {
530                this.name = name;
531        }
532
533
534        /**
535         * @return the authUser
536         */
537        public String getAuthUser() {
538                return authUser;
539        }
540
541
542        /**
543         * @param authUser the authUser to set
544         */
545        public void setAuthUser(String authUser) {
546                this.authUser = authUser;
547        }
548
549
550        /**
551         * @return the authPassword
552         */
553        public String getAuthPassword() {
554                return authPassword;
555        }
556
557
558        /**
559         * @param authPassword the authPassword to set
560         */
561        public void setAuthPassword(String authPassword) {
562                this.authPassword = authPassword;
563        }
564
565
566        /**
567         * @return the userAgent
568         */
569        public String getUserAgent() {
570                return userAgent;
571        }
572
573
574        /**
575         * @param userAgent the userAgent to set
576         */
577        public void setUserAgent(String userAgent) {
578                this.userAgent = userAgent;
579        }
580
581
582        /**
583         * @return the proxyserver
584         */
585        public String getProxyserver() {
586                return proxyserver;
587        }
588
589
590        /**
591         * @return the proxyport
592         */
593        public int getProxyport() {
594                return proxyport;
595        }
596
597
598        /**
599         * @return the proxyuser
600         */
601        public String getProxyuser() {
602                return proxyuser;
603        }
604
605
606        /**
607         * @return the proxypassword
608         */
609        public String getProxypassword() {
610                return proxypassword;
611        }
612
613
614        public boolean hasProxy() {
615                return !StringUtil.isEmpty(proxyserver);
616        }
617
618
619        /**
620         * @return the localUrl
621         */
622        public boolean getLocalUrl() {
623                return localUrl;
624        }
625
626
627        /**
628         * @param localUrl the localUrl to set
629         */
630        public void setLocalUrl(boolean localUrl) {
631                this.localUrl = localUrl;
632        }
633
634
635        /**
636         * @return the bookmark
637         */
638        public boolean getBookmark() {
639                return bookmark;
640        }
641
642
643        /**
644         * @param bookmark the bookmark to set
645         */
646        public void setBookmark(boolean bookmark) {
647                this.bookmark = bookmark;
648        }
649
650
651        /**
652         * @return the htmlBookmark
653         */
654        public boolean getHtmlBookmark() {
655                return htmlBookmark;
656        }
657
658
659        /**
660         * @param htmlBookmark the htmlBookmark to set
661         */
662        public void setHtmlBookmark(boolean htmlBookmark) {
663                this.htmlBookmark = htmlBookmark;
664        }
665
666}