openhtmltopdf icon indicating copy to clipboard operation
openhtmltopdf copied to clipboard

SVG TextPath causes StringIndexOutOfBoundsException

Open nstuivenberg opened this issue 3 years ago • 0 comments

Hello,

We are trying to render a SVG with a textPath element in a PDF, but we keep getting the following error: java.lang.StringIndexOutOfBoundsException: String index out of range: 0

Note: The problem is caused by the SVG file, so I am not 100% sure it should be an issue here or on batik. Any other tested SVG works, the issue only comes up when using the textPath-element.

Note 2: We develop in Kotlin, but we do not believe the issue is caused by that.

We tried textPath without attributes and/or different attributes. We tried to move the referring path-element to the parent, but all to no avail.

Fix or workaround would be appreciated.

com.openhtmltopdf.load INFO:: TIME: parse stylesheets 47ms
com.openhtmltopdf.match INFO:: media = print
com.openhtmltopdf.match INFO:: Matcher created with 160 selectors
com.openhtmltopdf.general INFO:: Using fast-mode renderer. Prepare to fly.
java.lang.StringIndexOutOfBoundsException: String index out of range: 0
	at java.base/java.lang.StringLatin1.charAt(StringLatin1.java:47)
	at java.base/java.lang.String.charAt(String.java:693)
	at org.apache.batik.bridge.URIResolver.getNode(URIResolver.java:99)
	at org.apache.batik.bridge.BridgeContext.getReferencedNode(BridgeContext.java:760)
	at org.apache.batik.bridge.BridgeContext.getReferencedElement(BridgeContext.java:804)
	at org.apache.batik.bridge.SVGTextPathElementBridge.createTextPath(SVGTextPathElementBridge.java:70)
	at org.apache.batik.bridge.SVGTextElementBridge.fillAttributedStringBuffer(SVGTextElementBridge.java:943)
	at org.apache.batik.bridge.SVGTextElementBridge.buildAttributedString(SVGTextElementBridge.java:850)
	at org.apache.batik.bridge.SVGTextElementBridge.computeLaidoutText(SVGTextElementBridge.java:630)
	at org.apache.batik.bridge.SVGTextElementBridge.buildGraphicsNode(SVGTextElementBridge.java:286)
	at org.apache.batik.bridge.GVTBuilder.buildGraphicsNode(GVTBuilder.java:224)
	at org.apache.batik.bridge.GVTBuilder.buildComposite(GVTBuilder.java:171)
	at org.apache.batik.bridge.GVTBuilder.build(GVTBuilder.java:82)
	at org.apache.batik.transcoder.SVGAbstractTranscoder.transcode(SVGAbstractTranscoder.java:208)
	at com.openhtmltopdf.svgsupport.PDFTranscoder.transcode(PDFTranscoder.java:278)
	at org.apache.batik.transcoder.XMLAbstractTranscoder.transcode(XMLAbstractTranscoder.java:142)
	at org.apache.batik.transcoder.SVGAbstractTranscoder.transcode(SVGAbstractTranscoder.java:156)
	at com.openhtmltopdf.svgsupport.BatikSVGImage.drawSVG(BatikSVGImage.java:231)
	at com.openhtmltopdf.pdfboxout.PdfBoxSVGReplacedElement.paint(PdfBoxSVGReplacedElement.java:70)
	at com.openhtmltopdf.pdfboxout.PdfBoxFastOutputDevice.paintReplacedElement(PdfBoxFastOutputDevice.java:275)
	at com.openhtmltopdf.render.displaylist.DisplayListPainter.paintReplacedElement(DisplayListPainter.java:169)
	at com.openhtmltopdf.render.displaylist.DisplayListPainter.paintReplacedElements(DisplayListPainter.java:151)
	at com.openhtmltopdf.render.displaylist.DisplayListPainter.paint(DisplayListPainter.java:268)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.paintPageFast(PdfBoxRenderer.java:936)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.writePDFFast(PdfBoxRenderer.java:628)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.createPdfFast(PdfBoxRenderer.java:564)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.createPDF(PdfBoxRenderer.java:490)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.createPDF(PdfBoxRenderer.java:427)
	at com.openhtmltopdf.pdfboxout.PdfBoxRenderer.createPDF(PdfBoxRenderer.java:409)
	at com.openhtmltopdf.pdfboxout.PdfRendererBuilder.run(PdfRendererBuilder.java:46)
	at nl.companyname.customername.controller.ReportController.downloadReport(ReportController.kt:169)
	at nl.companyname.customername.controller.ReportController$$FastClassBySpringCGLIB$$f9dac0a1.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor$1.proceed(AopAllianceAnnotationsAuthorizingMethodInterceptor.java:82)
	at org.apache.shiro.authz.aop.AuthorizingMethodInterceptor.invoke(AuthorizingMethodInterceptor.java:39)
	at org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor.invoke(AopAllianceAnnotationsAuthorizingMethodInterceptor.java:115)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
	at nl.companyname.customername.controller.ReportController$$EnhancerBySpringCGLIB$$6f581fb6.downloadReport(<generated>)
	at nl.companyname.customername.controller.ReportController$$FastClassBySpringCGLIB$$f9dac0a1.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor$1.proceed(AopAllianceAnnotationsAuthorizingMethodInterceptor.java:82)
	at org.apache.shiro.authz.aop.AuthorizingMethodInterceptor.invoke(AuthorizingMethodInterceptor.java:39)
	at org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor.invoke(AopAllianceAnnotationsAuthorizingMethodInterceptor.java:115)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
	at nl.companyname.customername.controller.ReportController$$EnhancerBySpringCGLIB$$87b8a28a.downloadReport(<generated>)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:450)
	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:834)

We tried our own SVG (piece of it below) and the one from Mozille, found here: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/textPath

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="600"
     height="600" style="position: absolute;">
    <g transform="translate(300,300)">
        <g class="slice">
            <path d="M-33.26240508564892,-69.07525016745126A76.66666666666667,76.66666666666667,0,0,1,-1.4083438190194563e-14,-76.66666666666667L-4.592425496802574e-15,-25A25,25,0,0,0,-10.846436440972475,-22.52453809808193Z"
                  style="fill: rgb(57, 59, 121); stroke: grey;"> </path>
            <text transform="translate(-19.506421979199633,-85.46896481187684)" dy=".35em" text-anchor="middle"
                  font-size="12px" font-family="arial" fill="black">2
            </text>
            <path id="path0"
                  d="M-110.63365169791925,-229.7502886004357A255,255,0,0,1,-4.684274006738626e-14,-255L-4.133182947122317e-14,-225A225,225,0,0,0,-97.61792796875227,-202.72084288273737Z"
                  style="fill: rgb(57, 59, 121); stroke: grey;"> </path>
            <text letter-spacing="1.5" dy="1.3em" font-size="8px" font-family="arial" fill="white">
                <textPath startOffset="5px" xlink:href="#path0">Wil sturen</textPath>
            </text>
        </g>
    </g>
</svg>

Our code is as follows:

    @PostMapping("/report/download/{id}", produces = [MediaType.APPLICATION_PDF_VALUE])
    fun downloadReportChangedForIssue() : ResponseEntity<ByteArray> {

        val document = getHtmlDocument()
        val byteArrayOutputStream =  ByteArrayOutputStream()
        try {
            val builder = PdfRendererBuilder()
            builder.withW3cDocument(document, "/")
            builder.toStream(byteArrayOutputStream)
            builder.useSVGDrawer(BatikSVGDrawer())
            builder.run()
            val headers = HttpHeaders()
            headers.add("Content-Disposition", "inline; filename=test.pdf")
            headers.add("Content-Type", "application/pdf")
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .headers(headers)
                .body(byteArrayOutputStream.toByteArray())
        } catch(error: Exception) {
            System.err.println(error.printStackTrace())
        } finally {
            byteArrayOutputStream.close()
        }
        throw RuntimeException("Error")
    }

    private fun getHtmlDocument(base64Img: String): Document {
       val simpleSvg = "<svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n" +
                "\n" +
                "  <!-- to hide the path, it is usually wrapped in a <defs> element -->\n" +
                "  <!-- <defs> -->\n" +
                "  <path id=\"MyPath\" fill=\"none\" stroke=\"red\"\n" +
                "        d=\"M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50\" />\n" +
                "  <!-- </defs> -->\n" +
                "\n" +
                "  <text>\n" +
                "    <textPath href=\"#MyPath\">\n" +
                "      Quick brown fox jumps over the lazy dog.\n" +
                "    </textPath>\n" +
                "  </text>\n" +
                "\n" +
                "</svg>"

        val jSoupDocument = Jsoup.parse(
            "<html><body><h1>Report</h1>" +
                    simpleSvg +
                    "<p>Hello World</p></body></html>", "", Parser.xmlParser()
        )
        val w3CDom = W3CDom();
        return w3CDom.fromJsoup(jSoupDocument)
    }

Extra info

pom.xml

	<properties>
		<java.version>11</java.version>
		<kotlin.version>1.5.30</kotlin.version>
		<jacoco.plugin.version>0.8.7</jacoco.plugin.version>
		<logstash-logback-encoder.version>6.3</logstash-logback-encoder.version>
		<openhtml.version>1.0.10</openhtml.version>
	</properties>

        <dependencies>
		<!-- PDF GENERATION -->
		<dependency>
			<groupId>com.openhtmltopdf</groupId>
			<artifactId>openhtmltopdf-core</artifactId>
			<version>${openhtml.version}</version>
		</dependency>
		<dependency>
			<groupId>com.openhtmltopdf</groupId>
			<artifactId>openhtmltopdf-pdfbox</artifactId>
			<version>${openhtml.version}</version>
		</dependency>
		<dependency>
			<groupId>com.openhtmltopdf</groupId>
			<artifactId>openhtmltopdf-java2d</artifactId>
			<version>${openhtml.version}</version>
		</dependency>
		<dependency>
			<groupId>com.openhtmltopdf</groupId>
			<artifactId>openhtmltopdf-slf4j</artifactId>
			<version>${openhtml.version}</version>
		</dependency>
		<dependency>
			<groupId>org.jsoup</groupId>
			<artifactId>jsoup</artifactId>
			<version>1.14.3</version>
		</dependency>

		<dependency>
		<!-- SVG support plugin. -->
		<groupId>com.openhtmltopdf</groupId>
		<artifactId>openhtmltopdf-svg-support</artifactId>
		<version>${openhtml.version}</version>
	</dependency>
   </dependencies>

nstuivenberg avatar Nov 12 '21 14:11 nstuivenberg