Feature Request: Allow svg images #889

Open
opened 2026-02-04 22:44:46 +03:00 by OVERLORD · 6 comments
Owner

Originally created by @TBK on GitHub (Nov 1, 2018).

Describe the feature you'd like
Make it possible to upload svg images.

Describe the benefits this feature would bring to BookStack users
Scalable Vector Graphics FTW 👍

Additional context
This is what happens at the moment:
image

Originally created by @TBK on GitHub (Nov 1, 2018). **Describe the feature you'd like** Make it possible to upload svg images. **Describe the benefits this feature would bring to BookStack users** Scalable Vector Graphics FTW 👍 **Additional context** This is what happens at the moment: ![image](https://user-images.githubusercontent.com/858296/47861734-bea3d700-ddf3-11e8-83d6-cce173c4db2e.png)
OVERLORD added the 🔨 Feature Request🏭 Back-End💻 Front-End labels 2026-02-04 22:44:46 +03:00
Author
Owner

@wouterloedeman commented on GitHub (Nov 1, 2018):

I agree. Would be nice if they could be used as application logo as well!

@wouterloedeman commented on GitHub (Nov 1, 2018): I agree. Would be nice if they could be used as application logo as well!
Author
Owner

@xblitz commented on GitHub (Nov 8, 2018):

Yes! Just tried using SVG logo since the current requirement of 43px Height looks bad on HiDPI Screens (4k, phones)

@xblitz commented on GitHub (Nov 8, 2018): Yes! Just tried using SVG logo since the current requirement of 43px Height looks bad on HiDPI Screens (4k, phones)
Author
Owner

@bharatrajagopalan commented on GitHub (Dec 16, 2018):

Hi

EDIT: a slight tweak that lets me get rid of the div element by using the beforeInject hook of SVGInject

Not perfect but managed to get interactive SVG working with the following

I am using plantuml to generate SVG for UML diagrams

I upload svgs as attachments and then am using styles to add a nice grey background. i am using the excellent SVGinject javascript to find img tags with class plantsvg and replace it with the SVG content. Consequently as SVG is basically html text, i am using an enclosing div to center it

Details

Paste this into Settings > Custom HTML Head

image
<style> 
 .plantsvg  {
   height:auto;
   background-color:#FAFAFA;
   padding:20px;
   max-width:100%;
 }

 .plantdiv {
   text-align:center;

 } 
</style>

<script>


!function(o,l){var r,a,s="createElement",y="getElementsByTagName",g="length",E="style",d="title",b="undefined",h="setAttribute",k="getAttribute",w=null,x="__svgInject",A="--inject-",C=new RegExp(A+"\\d+","g"),S="LOAD_FAIL",t="SVG_NOT_SUPPORTED",I="SVG_INVALID",v=["src","alt","onload","onerror"],L=l[s]("a"),j=typeof SVGRect!=b,f={useCache:!0,copyAttributes:!0,makeIdsUnique:!0},G={clipPath:["clip-path"],"color-profile":w,cursor:w,filter:w,linearGradient:["fill","stroke"],marker:["marker",
"marker-end","marker-mid","marker-start"],mask:w,pattern:["fill","stroke"],radialGradient:["fill","stroke"]},u=1,c=2,N=1;function O(e){return(r=r||new XMLSerializer).serializeToString(e)}function T(e){var t,r,n,i,o=A+N++,a=e.querySelectorAll("[id]"),f={},u=[],c=!1;for(n=0;n<a[g];n++)(r=(t=a[n]).localName)in G&&(c=!0,f[r]=1,t.id+=o,["xlink:href","href"].forEach(function(e){var r=t[k](e);/^\s*#/.test(r)&&t[h](e,r.trim()+o)}));for(r in f)(G[r]||[r]).forEach(function(e){u.indexOf(e)<0&&u.push(e)})
;if(u[g]){u.push(E);var l,s,d,v,p=/url\("?#([a-zA-Z][\w:.-]*)"?\)/g,m=e[y]("*");for(n=0;n<m[g];n++)if((l=m[n]).localName==E)(v=(d=l.textContent)&&d.replace(p,"url(#$1"+o+")"))!==d&&(l.textContent=v);else if(l.hasAttributes())for(i=0;i<u[g];i++)s=u[i],(v=(d=l[k](s))&&d.replace(p,"url(#$1"+o+")"))!==d&&l[h](s,v)}return c}function P(e,r,t,n){if(r){r[h]("data-inject-url",t);var i=e.parentNode;if(i){n.copyAttributes&&function c(e,r){for(var t,n,i,o=e.attributes,a=0;a<o[g];a++)if(n=(t=o[a]).name,
-1==v.indexOf(n))if(i=t.value,n==d){var f,u=r.firstElementChild;u&&u.localName.toLowerCase()==d?f=u:(f=l[s+"NS"]("http://www.w3.org/2000/svg",d),r.insertBefore(f,u)),f.textContent=i}else r[h](n,i)}(e,r);var o=n.beforeInject,a=o&&o(e,r)||r;i.replaceChild(a,e),e[x]=u,m(e);var f=n.afterInject;f&&f(e,a)}}else _(e,n)}function p(){for(var e={},r=arguments,t=0;t<r[g];t++){var n=r[t];for(var i in n)n.hasOwnProperty(i)&&(e[i]=n[i])}return e}function V(e,r){if(r){var t;try{t=function i(e){return(
a=a||new DOMParser).parseFromString(e,"text/xml")}(e)}catch(o){return w}return t[y]("parsererror")[g]?w:t.documentElement}var n=l.createElement("div");return n.innerHTML=e,n.firstElementChild}function m(e){e.removeAttribute("onload")}function n(e){console.error("SVGInject: "+e)}function i(e,r,t){e[x]=c,t.onFail?t.onFail(e,r):n(r)}function _(e,r){m(e),i(e,I,r)}function D(e,r){m(e),i(e,t,r)}function F(e,r){i(e,S,r)}function M(e){e.onload=w,e.onerror=w}function q(e){n("no img element")}
var e=function R(e,r){var t=p(f,r),h={};function n(a,f){f=p(t,f);var e=function(r){var e=function(){var e=f.onAllFinish;e&&e(),r&&r()};if(a&&typeof a[g]!=b){var t=0,n=a[g];if(0==n)e();else for(var i=function(){++t==n&&e()},o=0;o<n;o++)u(a[o],f,i)}else u(a,f,e)};return typeof Promise==b?e():new Promise(e)}function u(u,c,e){if(u){var r=u[x];if(r)Array.isArray(r)?r.push(e):e();else{if(M(u),!j)return D(u,c),void e();var t=c.beforeLoad,n=t&&t(u)||u[k]("src");if(!n)return""===n&&F(u,c),void e()
;var i=[];u[x]=i;var l=function(){e(),i.forEach(function(e){e()})},s=function f(e){return L.href=e,L.href}(n),d=c.useCache,v=c.makeIdsUnique,p=function(r){d&&(h[s].forEach(function(e){e(r)}),h[s]=r)};if(d){var o,a=function(e){if(e===S)F(u,c);else if(e===I)_(u,c);else{var r,t=e[0],n=e[1],i=e[2];v&&(t===w?(t=T(r=V(n,!1)),e[0]=t,e[2]=t&&O(r)):t&&(n=function o(e){return e.replace(C,A+N++)}(i))),r=r||V(n,!1),P(u,r,s,c)}l()};if(typeof(o=h[s])!=b)return void(o.isCallbackQueue?o.push(a):a(o));(o=[]
).isCallbackQueue=!0,h[s]=o}!function m(e,r,t){if(e){var n=new XMLHttpRequest;n.onreadystatechange=function(){if(4==n.readyState){var e=n.status;200==e?r(n.responseXML,n.responseText.trim()):400<=e?t():0==e&&t()}},n.open("GET",e,!0),n.send()}}(s,function(e,r){var t=e instanceof Document?e.documentElement:V(r,!0),n=c.afterLoad;if(n){var i=n(t,r)||t;if(i){var o="string"==typeof i;r=o?i:O(t),t=o?V(i,!0):i}}if(t instanceof SVGElement){var a=w;if(v&&(a=T(t)),d){var f=a&&O(t);p([a,r,f])}P(u,t,s,c)
}else _(u,c),p(I);l()},function(){F(u,c),p(S),l()})}}else q()}return j&&function i(e){var r=l[y]("head")[0];if(r){var t=l[s](E);t.type="text/css",t.appendChild(l.createTextNode(e)),r.appendChild(t)}}('img[onload^="'+e+'("]{visibility:hidden;}'),n.setOptions=function(e){t=p(t,e)},n.create=R,n.err=function(e,r){e?e[x]!=c&&(M(e),j?(m(e),F(e,t)):D(e,t),r&&(m(e),e.src=r)):q()},o[e]=n}("SVGInject");"object"==typeof module&&"object"==typeof module.exports&&(module.exports=e)}(window,document);

</script>

<script>
    SVGInject.setOptions({
     
      beforeInject: function(img, svg) {
        // svg text alignment set via div
        //ensure that svg aspect ratio maintained
        svg.style["max-width"]="100%";
  
        var div = document.createElement('div');
        div.style["text-align"]="center"; 
        div.appendChild(svg);
        return div;
      }, 
      
      
    });

</script>

<script>
addEventListener("load", function() {  SVGInject(document.querySelector("img.plantsvg")); })
</script>

You need to use the Markdown editor to get this working - Settings > Page Editor - Select Markdown
image

And then in your editor - paste the following - change the src to the path to your SVG file - in my case it is an attachment

image
  <img src="/attachments/10" class="plantsvg"  />

And the final result looks like this when you save

image

It is interactive - i.e. i can click links & select text

image image

@bharatrajagopalan commented on GitHub (Dec 16, 2018): Hi EDIT: a slight tweak that lets me get rid of the **div** element by using the beforeInject hook of SVGInject Not perfect but managed to get interactive SVG working with the following I am using plantuml to generate SVG for UML diagrams I upload svgs as attachments and then am using styles to add a nice grey background. i am using the excellent SVGinject javascript to find img tags with class plantsvg and replace it with the SVG content. Consequently as SVG is basically html text, i am using an enclosing div to center it <details><summary>Details</summary> <p> Paste this into Settings > Custom HTML Head <img width="818" alt="image" src="https://user-images.githubusercontent.com/15735432/50048646-d0062180-00c9-11e9-946d-e0e3c1864f67.png"> ```javascript <style> .plantsvg { height:auto; background-color:#FAFAFA; padding:20px; max-width:100%; } .plantdiv { text-align:center; } </style> <script> !function(o,l){var r,a,s="createElement",y="getElementsByTagName",g="length",E="style",d="title",b="undefined",h="setAttribute",k="getAttribute",w=null,x="__svgInject",A="--inject-",C=new RegExp(A+"\\d+","g"),S="LOAD_FAIL",t="SVG_NOT_SUPPORTED",I="SVG_INVALID",v=["src","alt","onload","onerror"],L=l[s]("a"),j=typeof SVGRect!=b,f={useCache:!0,copyAttributes:!0,makeIdsUnique:!0},G={clipPath:["clip-path"],"color-profile":w,cursor:w,filter:w,linearGradient:["fill","stroke"],marker:["marker", "marker-end","marker-mid","marker-start"],mask:w,pattern:["fill","stroke"],radialGradient:["fill","stroke"]},u=1,c=2,N=1;function O(e){return(r=r||new XMLSerializer).serializeToString(e)}function T(e){var t,r,n,i,o=A+N++,a=e.querySelectorAll("[id]"),f={},u=[],c=!1;for(n=0;n<a[g];n++)(r=(t=a[n]).localName)in G&&(c=!0,f[r]=1,t.id+=o,["xlink:href","href"].forEach(function(e){var r=t[k](e);/^\s*#/.test(r)&&t[h](e,r.trim()+o)}));for(r in f)(G[r]||[r]).forEach(function(e){u.indexOf(e)<0&&u.push(e)}) ;if(u[g]){u.push(E);var l,s,d,v,p=/url\("?#([a-zA-Z][\w:.-]*)"?\)/g,m=e[y]("*");for(n=0;n<m[g];n++)if((l=m[n]).localName==E)(v=(d=l.textContent)&&d.replace(p,"url(#$1"+o+")"))!==d&&(l.textContent=v);else if(l.hasAttributes())for(i=0;i<u[g];i++)s=u[i],(v=(d=l[k](s))&&d.replace(p,"url(#$1"+o+")"))!==d&&l[h](s,v)}return c}function P(e,r,t,n){if(r){r[h]("data-inject-url",t);var i=e.parentNode;if(i){n.copyAttributes&&function c(e,r){for(var t,n,i,o=e.attributes,a=0;a<o[g];a++)if(n=(t=o[a]).name, -1==v.indexOf(n))if(i=t.value,n==d){var f,u=r.firstElementChild;u&&u.localName.toLowerCase()==d?f=u:(f=l[s+"NS"]("http://www.w3.org/2000/svg",d),r.insertBefore(f,u)),f.textContent=i}else r[h](n,i)}(e,r);var o=n.beforeInject,a=o&&o(e,r)||r;i.replaceChild(a,e),e[x]=u,m(e);var f=n.afterInject;f&&f(e,a)}}else _(e,n)}function p(){for(var e={},r=arguments,t=0;t<r[g];t++){var n=r[t];for(var i in n)n.hasOwnProperty(i)&&(e[i]=n[i])}return e}function V(e,r){if(r){var t;try{t=function i(e){return( a=a||new DOMParser).parseFromString(e,"text/xml")}(e)}catch(o){return w}return t[y]("parsererror")[g]?w:t.documentElement}var n=l.createElement("div");return n.innerHTML=e,n.firstElementChild}function m(e){e.removeAttribute("onload")}function n(e){console.error("SVGInject: "+e)}function i(e,r,t){e[x]=c,t.onFail?t.onFail(e,r):n(r)}function _(e,r){m(e),i(e,I,r)}function D(e,r){m(e),i(e,t,r)}function F(e,r){i(e,S,r)}function M(e){e.onload=w,e.onerror=w}function q(e){n("no img element")} var e=function R(e,r){var t=p(f,r),h={};function n(a,f){f=p(t,f);var e=function(r){var e=function(){var e=f.onAllFinish;e&&e(),r&&r()};if(a&&typeof a[g]!=b){var t=0,n=a[g];if(0==n)e();else for(var i=function(){++t==n&&e()},o=0;o<n;o++)u(a[o],f,i)}else u(a,f,e)};return typeof Promise==b?e():new Promise(e)}function u(u,c,e){if(u){var r=u[x];if(r)Array.isArray(r)?r.push(e):e();else{if(M(u),!j)return D(u,c),void e();var t=c.beforeLoad,n=t&&t(u)||u[k]("src");if(!n)return""===n&&F(u,c),void e() ;var i=[];u[x]=i;var l=function(){e(),i.forEach(function(e){e()})},s=function f(e){return L.href=e,L.href}(n),d=c.useCache,v=c.makeIdsUnique,p=function(r){d&&(h[s].forEach(function(e){e(r)}),h[s]=r)};if(d){var o,a=function(e){if(e===S)F(u,c);else if(e===I)_(u,c);else{var r,t=e[0],n=e[1],i=e[2];v&&(t===w?(t=T(r=V(n,!1)),e[0]=t,e[2]=t&&O(r)):t&&(n=function o(e){return e.replace(C,A+N++)}(i))),r=r||V(n,!1),P(u,r,s,c)}l()};if(typeof(o=h[s])!=b)return void(o.isCallbackQueue?o.push(a):a(o));(o=[] ).isCallbackQueue=!0,h[s]=o}!function m(e,r,t){if(e){var n=new XMLHttpRequest;n.onreadystatechange=function(){if(4==n.readyState){var e=n.status;200==e?r(n.responseXML,n.responseText.trim()):400<=e?t():0==e&&t()}},n.open("GET",e,!0),n.send()}}(s,function(e,r){var t=e instanceof Document?e.documentElement:V(r,!0),n=c.afterLoad;if(n){var i=n(t,r)||t;if(i){var o="string"==typeof i;r=o?i:O(t),t=o?V(i,!0):i}}if(t instanceof SVGElement){var a=w;if(v&&(a=T(t)),d){var f=a&&O(t);p([a,r,f])}P(u,t,s,c) }else _(u,c),p(I);l()},function(){F(u,c),p(S),l()})}}else q()}return j&&function i(e){var r=l[y]("head")[0];if(r){var t=l[s](E);t.type="text/css",t.appendChild(l.createTextNode(e)),r.appendChild(t)}}('img[onload^="'+e+'("]{visibility:hidden;}'),n.setOptions=function(e){t=p(t,e)},n.create=R,n.err=function(e,r){e?e[x]!=c&&(M(e),j?(m(e),F(e,t)):D(e,t),r&&(m(e),e.src=r)):q()},o[e]=n}("SVGInject");"object"==typeof module&&"object"==typeof module.exports&&(module.exports=e)}(window,document); </script> <script> SVGInject.setOptions({ beforeInject: function(img, svg) { // svg text alignment set via div //ensure that svg aspect ratio maintained svg.style["max-width"]="100%"; var div = document.createElement('div'); div.style["text-align"]="center"; div.appendChild(svg); return div; }, }); </script> <script> addEventListener("load", function() { SVGInject(document.querySelector("img.plantsvg")); }) </script> ``` You need to use the Markdown editor to get this working - Settings > Page Editor - Select Markdown <img width="377" alt="image" src="https://user-images.githubusercontent.com/15735432/50048643-c1b80580-00c9-11e9-846f-3a883acb1747.png"> And then in your editor - paste the following - change the src to the path to your SVG file - in my case it is an attachment <img width="1396" alt="image" src="https://user-images.githubusercontent.com/15735432/50053264-e5129d00-0129-11e9-8c50-f17105ee5ae8.png"> ``` <img src="/attachments/10" class="plantsvg" /> ``` And the final result looks like this when you save <img width="1377" alt="image" src="https://user-images.githubusercontent.com/15735432/50048665-270bf680-00ca-11e9-8c82-b41413344d96.png"> It is interactive - i.e. i can click links & select text <img width="963" alt="image" src="https://user-images.githubusercontent.com/15735432/50048681-794d1780-00ca-11e9-875c-3340bca24ca9.png"> <img width="1364" alt="image" src="https://user-images.githubusercontent.com/15735432/50048685-971a7c80-00ca-11e9-8c47-a2a24df2d635.png"> </p> </details>
Author
Owner

@HacKanCuBa commented on GitHub (Mar 16, 2019):

I would love this fix too!
Does the fix mentioned above requires ALLOW_CONTENT_SCRIPTS to be true? cc @bharatrajagopalan

@HacKanCuBa commented on GitHub (Mar 16, 2019): I would love this fix too! Does the fix mentioned above requires `ALLOW_CONTENT_SCRIPTS` to be true? cc @bharatrajagopalan
Author
Owner

@WarriorXK commented on GitHub (Jan 4, 2021):

Is there any indication that this would be added? I have a lot of diagrams and it would be amazing if we could retain the SVG quality.

@WarriorXK commented on GitHub (Jan 4, 2021): Is there any indication that this would be added? I have a lot of diagrams and it would be amazing if we could retain the SVG quality.
Author
Owner

@tinsever commented on GitHub (Dec 17, 2025):

Will this ever come? o.o

@tinsever commented on GitHub (Dec 17, 2025): Will this ever come? o.o
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/BookStack#889