Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(renderer): support 'data-uri' in images #877

Merged
merged 1 commit into from
Nov 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.