Skip to content

Commit

Permalink
feat(renderer): support 'data-uri' in images (#877)
Browse files Browse the repository at this point in the history
include the content of the image file (encoded in base64)
in the output, unless the file is not found or is not readable.

Fixes #853

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon authored Nov 28, 2021
1 parent 0b72891 commit a68a438
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 17 deletions.
2 changes: 1 addition & 1 deletion pkg/renderer/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// Context is a custom implementation of the standard golang context.Context interface,
// which carries the types.Document which is being processed
type Context struct {
Config *configuration.Configuration
Config *configuration.Configuration // TODO: use composition (remove the `Config` field)
// TableOfContents exists even if the document did not specify the `:toc:` attribute.
// It will take into account the configured `:toclevels:` attribute value.
TableOfContents types.TableOfContents
Expand Down
2 changes: 1 addition & 1 deletion pkg/renderer/sgml/html5/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const (
`{{ if .Link }}</a>{{ end }}` +
`</span>`

iconImageTmpl = `<img src="{{ .Path }}"` +
iconImageTmpl = `<img src="{{ .Src }}"` +
`{{ if .Alt }} alt="{{ .Alt }}{{ end }}"` +
`{{ if .Width }} width="{{ .Width }}"{{ end }}` +
`{{ if .Height }} height="{{ .Height }}"{{ end }}` +
Expand Down
4 changes: 2 additions & 2 deletions pkg/renderer/sgml/html5/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package html5
const (
blockImageTmpl = `<div{{ if .ID }} id="{{ .ID }}"{{ end }} class="imageblock{{ if .Roles }} {{ .Roles }}{{ end }}">
<div class="content">
{{ if ne .Href "" }}<a class="image" href="{{ .Href }}">{{ end }}<img src="{{ .Path }}" alt="{{ .Alt }}"{{ if .Width }} width="{{ .Width }}"{{ end }}{{ if .Height }} height="{{ .Height }}"{{ end }}>{{ if ne .Href "" }}</a>{{ end }}
{{ if ne .Href "" }}<a class="image" href="{{ .Href }}">{{ end }}<img src="{{ .Src }}" alt="{{ .Alt }}"{{ if .Width }} width="{{ .Width }}"{{ end }}{{ if .Height }} height="{{ .Height }}"{{ end }}>{{ if ne .Href "" }}</a>{{ end }}
</div>{{ if .Title }}
<div class="title">{{ .Caption }}{{ .Title }}</div>
{{ else }}
{{ end }}</div>
`
inlineImageTmpl = `<span class="image{{ if .Roles }} {{ .Roles }}{{ end }}">{{ if ne .Href "" }}<a class="image" href="{{ .Href }}">{{ end }}<img src="{{ .Path }}" alt="{{ .Alt }}"{{ if .Width }} width="{{ .Width }}"{{ end }}{{ if .Height }} height="{{ .Height }}"{{ end }}{{ if .Title }} title="{{ .Title }}"{{ end }}>{{ if ne .Href "" }}</a>{{ end }}</span>`
inlineImageTmpl = `<span class="image{{ if .Roles }} {{ .Roles }}{{ end }}">{{ if ne .Href "" }}<a class="image" href="{{ .Href }}">{{ end }}<img src="{{ .Src }}" alt="{{ .Alt }}"{{ if .Width }} width="{{ .Width }}"{{ end }}{{ if .Height }} height="{{ .Height }}"{{ end }}{{ if .Title }} title="{{ .Title }}"{{ end }}>{{ if ne .Href "" }}</a>{{ end }}</span>`
)
66 changes: 66 additions & 0 deletions pkg/renderer/sgml/html5/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,70 @@ image::file:///bar/foo.png[]`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})
})

Context("data-uri", func() {
// see https://docs.asciidoctor.org/asciidoctor/latest/html-backend/manage-images/#allow-uri-read-attribute

It("inline image with imagesdir", func() {
source := `
:imagesdir: ../../../../test/images
:data-uri:
image:favicon-glasses-16x16.png[Glasses]`

expected := `<div class="paragraph">
<p><span class="image"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABSklEQVQ4je2Rz0oCURSHT7ipd5ByExGmc28zc++M946OIrhxGgxHqE2N7XJVy8I3cCO4dB2hLnwC+4MY+A6CLgUV3BrObVFOYZugrR+c1fdbnPM7ABt+sgUAgT/kAl/ZTxBhL5jwuZw6WWq261G7uJCT1hhR3pUIr0uE1xHlXTlpjaldXGi268kpa4kJnyPCn0FS2SM7vxX55lQ4rZlwWjORb0yEcXEnEOVDRPnQuLwX+cbk2zengp3dCET4Axwd06jhln25GrNUERLhOUllp2ap8ssbbllEZD0CwaC+o9lX7+sB3bn2wrK8e4hjezGn5K17zS4uQqHQNgAAYJp4tWp9f71stSewZr6tesK62c9We/6ZVq0vkJZ48ouUFB0jGu+omcJISecGiLA2QnR/5aMKO0CEtZV0bqBmCiNE452wGkP/f/wGAAD4AGCWrt/5+Pc0AAAAAElFTkSuQmCC" alt="Glasses"></span></p>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})

It("inline image not found", func() {
source := `
:imagesdir: ./path/to/somewhere/else
:data-uri:
image:favicon-glasses-16x16.png[Glasses]`

expected := `<div class="paragraph">
<p><span class="image"><img src="data:image/png;base64," alt="Glasses"></span></p>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
// TODO: check that the log/output contains a WARNING message (`image to embed not found or not readable`)
})

It("block image with imagesdir", func() {
source := `
:imagesdir: ../../../../test/images
:data-uri:
image::favicon-glasses-16x16.png[Glasses]`

expected := `<div class="imageblock">
<div class="content">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABSklEQVQ4je2Rz0oCURSHT7ipd5ByExGmc28zc++M946OIrhxGgxHqE2N7XJVy8I3cCO4dB2hLnwC+4MY+A6CLgUV3BrObVFOYZugrR+c1fdbnPM7ABt+sgUAgT/kAl/ZTxBhL5jwuZw6WWq261G7uJCT1hhR3pUIr0uE1xHlXTlpjaldXGi268kpa4kJnyPCn0FS2SM7vxX55lQ4rZlwWjORb0yEcXEnEOVDRPnQuLwX+cbk2zengp3dCET4Axwd06jhln25GrNUERLhOUllp2ap8ssbbllEZD0CwaC+o9lX7+sB3bn2wrK8e4hjezGn5K17zS4uQqHQNgAAYJp4tWp9f71stSewZr6tesK62c9We/6ZVq0vkJZ48ouUFB0jGu+omcJISecGiLA2QnR/5aMKO0CEtZV0bqBmCiNE452wGkP/f/wGAAD4AGCWrt/5+Pc0AAAAAElFTkSuQmCC" alt="Glasses">
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})

It("block image not found", func() {
source := `
:imagesdir: ./path/to/somewhere/else
:data-uri:
image::favicon-glasses-16x16.png[Glasses]`

expected := `<div class="imageblock">
<div class="content">
<img src="data:image/png;base64," alt="Glasses">
</div>
</div>
`
Expect(RenderHTML(source)).To(MatchHTML(expected))
// TODO: check that the log/output contains a WARNING message (`image to embed not found or not readable`)
})
})
})
4 changes: 2 additions & 2 deletions pkg/renderer/sgml/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (r *sgmlRenderer) renderIcon(ctx *renderer.Context, icon types.Icon, admoni
Flip string
Width string
Height string
Path string
Src string
Admonition bool
}{
Class: icon.Class,
Expand All @@ -111,7 +111,7 @@ func (r *sgmlRenderer) renderIcon(ctx *renderer.Context, icon types.Icon, admoni
Flip: icon.Attributes.GetAsStringWithDefault(types.AttrIconFlip, ""),
Link: icon.Attributes.GetAsStringWithDefault(types.AttrInlineLink, ""),
Window: icon.Attributes.GetAsStringWithDefault(types.AttrImageWindow, ""),
Path: renderIconPath(ctx, icon.Class),
Src: renderIconPath(ctx, icon.Class),
Admonition: admonition,
})
return string(s.String()), err
Expand Down
37 changes: 29 additions & 8 deletions pkg/renderer/sgml/image.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package sgml

import (
"encoding/base64"
"io/ioutil"
"net/url"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -50,13 +52,14 @@ func (r *sgmlRenderer) renderImageBlock(ctx *renderer.Context, img *types.ImageB
if err != nil {
return "", errors.Wrap(err, "unable to render image")
}
path := img.Location.Stringify()
alt, err := r.renderImageAlt(img.Attributes, path)
src := r.getImageSrc(ctx, img.Location)
alt, err := r.renderImageAlt(img.Attributes, src)
if err != nil {
return "", errors.Wrap(err, "unable to render image")
}
err = r.blockImage.Execute(result, struct {
ID string
Src string
Title string
ImageNumber int
Caption string
Expand All @@ -65,9 +68,9 @@ func (r *sgmlRenderer) renderImageBlock(ctx *renderer.Context, img *types.ImageB
Alt string
Width string
Height string
Path string
}{
ID: r.renderElementID(img.Attributes),
Src: src,
Title: title,
ImageNumber: number,
Caption: caption.String(),
Expand All @@ -76,7 +79,6 @@ func (r *sgmlRenderer) renderImageBlock(ctx *renderer.Context, img *types.ImageB
Alt: alt,
Width: img.Attributes.GetAsStringWithDefault(types.AttrWidth, ""),
Height: img.Attributes.GetAsStringWithDefault(types.AttrHeight, ""),
Path: path,
})

if err != nil {
Expand All @@ -92,8 +94,8 @@ func (r *sgmlRenderer) renderInlineImage(ctx *Context, img *types.InlineImage) (
return "", errors.Wrap(err, "unable to render image")
}
href := img.Attributes.GetAsStringWithDefault(types.AttrInlineLink, "")
path := img.Location.Stringify()
alt, err := r.renderImageAlt(img.Attributes, path)
src := r.getImageSrc(ctx, img.Location)
alt, err := r.renderImageAlt(img.Attributes, src)
if err != nil {
return "", errors.Wrap(err, "unable to render image")
}
Expand All @@ -103,21 +105,21 @@ func (r *sgmlRenderer) renderInlineImage(ctx *Context, img *types.InlineImage) (
}

err = r.inlineImage.Execute(result, struct {
Src string
Roles string
Title string
Href string
Alt string
Width string
Height string
Path string
}{
Src: src,
Title: title,
Roles: roles,
Href: href,
Alt: alt,
Width: img.Attributes.GetAsStringWithDefault(types.AttrWidth, ""),
Height: img.Attributes.GetAsStringWithDefault(types.AttrHeight, ""),
Path: path,
})

if err != nil {
Expand All @@ -129,6 +131,25 @@ func (r *sgmlRenderer) renderInlineImage(ctx *Context, img *types.InlineImage) (
return result.String(), nil
}

func (r *sgmlRenderer) getImageSrc(ctx *Context, location *types.Location) string {
src := location.Stringify()

// if Data URI is enables, then include the content of the file in the `src` attribute of the `<img>` tag
if !ctx.Attributes.Has("data-uri") {
return src
}
dir := filepath.Dir(ctx.Config.Filename)
src = filepath.Join(dir, src)
result := "data:image/" + strings.TrimPrefix(filepath.Ext(src), ".") + ";base64,"
data, err := ioutil.ReadFile(src)
if err != nil {
log.Warnf("image to embed not found or not readable: %s", src)
return result
}
result += base64.StdEncoding.EncodeToString(data)
return result
}

func (r *sgmlRenderer) renderImageAlt(attrs types.Attributes, path string) (string, error) {
if alt, found, err := attrs.GetAsString(types.AttrImageAlt); err != nil {
return "", errors.Wrap(err, "unable to render image")
Expand Down
2 changes: 1 addition & 1 deletion pkg/renderer/sgml/xhtml5/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const (
//
// Only the img tag needs to be made XHTML safe.

iconImageTmpl = `<img src="{{ .Path }}"` +
iconImageTmpl = `<img src="{{ .Src }}"` +
`{{ if .Alt }} alt="{{ .Alt }}{{ end }}"` +
`{{ if .Width }} width="{{ .Width }}"{{ end }}` +
`{{ if .Height }} height="{{ .Height }}"{{ end }}` +
Expand Down
4 changes: 2 additions & 2 deletions pkg/renderer/sgml/xhtml5/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const (
" class=\"imageblock{{ if .Roles }} {{ .Roles }}{{ end }}\">\n" +
"<div class=\"content\">\n" +
`{{ if .Href }}<a class="image" href="{{ .Href }}">{{ end }}` +
`<img src="{{ .Path }}" alt="{{ .Alt }}"` +
`<img src="{{ .Src }}" alt="{{ .Alt }}"` +
`{{ if .Width }} width="{{ .Width }}"{{ end }}` +
`{{ if .Height }} height="{{ .Height }}"{{ end }}` +
"/>{{ if .Href }}</a>{{ end }}\n" +
Expand All @@ -16,7 +16,7 @@ const (

inlineImageTmpl = `<span class="image{{ if .Roles }} {{ .Roles }}{{ end }}">` +
`{{ if .Href }}<a class="image" href="{{ .Href }}">{{ end }}` +
`<img src="{{ .Path }}" alt="{{ .Alt }}"` +
`<img src="{{ .Src }}" alt="{{ .Alt }}"` +
`{{ if .Width }} width="{{ .Width }}"{{ end }}` +
`{{ if .Height }} height="{{ .Height }}"{{ end }}` +
`{{ if .Title }} title="{{ .Title }}"{{ end }}` +
Expand Down
Binary file added test/images/favicon-glasses-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a68a438

Please sign in to comment.