create_image_block

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
8 messages Options
Reply | Threaded
Open this post in threaded view
|

create_image_block

ttmetro
I wrote a Block extension (code is below) that converts pyplot instructions to plots inserted into the document. Problems:
  1. create_image_block throws error "`image': undefined method `include?' for nil:NilClass". What is the correct calling syntax? I have the file name and caption.
  2. Once the entire document is parsed, python needs to be called (at_end method in code below). How can I get the asciidoctor processor to call at_end after parsing?
  3. Is there a way to get the path of the document being processed? Would be useful to put the generated figures in the right place.
Many thanks!
Bernhard

Code:

require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage
#
#   plot([1,2,3])
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl

  named :pyplot
  on_context :listing
  parse_content_as :simple
  name_attributes 'caption'

  PY_FILE = "pyplot.py"
  PL_FILE = "py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    if @@counter < 1
      # create PY_FILE with header
      File.open(PY_FILE, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
      end
    end

    @@counter += 1
    fig_file = PL_FILE % @@counter
    caption = (attrs.delete 'caption') || ''

    File.open(PY_FILE, 'a') do |fo|
      fo.puts
      fo.puts "%s #{fig_file}: #{caption}" % ('#'*5)
      fo.puts "clf()"
      fo.puts reader.source_lines
      fo.puts "tight_layout()"
      fo.puts "savefig('#{fig_file}')"
    end

    # create_image_block raises following error:
    # asciidoctor/converter/html5.rb:552:in `image': undefined method `include?' for nil:NilClass (NoMethodError)
    create_image_block parent, file_name:fig_file, title:caption
  end

  # call this after parsing entire document
  # (renders plots)
  def at_end
    system("python #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end

Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

mojavelinux
Administrator
@ttmetro,

Whenever you create an image block via the API, you must specify the target using the target attribute. The target is the path that is used in the src attribute of the <img> tag in the HTML output. For an example, see https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/lib/mathematical-treeprocessor/extension.rb#L40

To run code after parsing is complete, you need to use a tree processor.

When writing image files, it's best to follow what Asciidoctor Diagram does (since it also generates images to disk). This comes down to use the value of the imagesoutdir attribute if set, otherwise use the value of the to_dir option + imagesdir. See https://github.com/asciidoctor/asciidoctor-diagram/blob/master/lib/asciidoctor-diagram/extensions.rb#L245-L258

Cheers,

-Dan

On Mon, Sep 17, 2018 at 11:44 AM ttmetro [via Asciidoctor :: Discussion] <[hidden email]> wrote:
I wrote a Block extension (code is below) that converts pyplot instructions to plots inserted into the document. Problems:
  1. create_image_block throws error "`image': undefined method `include?' for nil:NilClass". What is the correct calling syntax? I have the file name and caption.
  2. Once the entire document is parsed, python needs to be called (at_end method in code below). How can I get the asciidoctor processor to call at_end after parsing?
  3. Is there a way to get the path of the document being processed? Would be useful to put the generated figures in the right place.
Many thanks!
Bernhard

Code:

require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage
#
#   plot([1,2,3])
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl

  named :pyplot
  on_context :listing
  parse_content_as :simple
  name_attributes 'caption'

  PY_FILE = "pyplot.py"
  PL_FILE = "py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    if @@counter < 1
      # create PY_FILE with header
      File.open(PY_FILE, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
      end
    end

    @@counter += 1
    fig_file = PL_FILE % @@counter
    caption = (attrs.delete 'caption') || ''

    File.open(PY_FILE, 'a') do |fo|
      fo.puts
      fo.puts "%s #{fig_file}: #{caption}" % ('#'*5)
      fo.puts "clf()"
      fo.puts reader.source_lines
      fo.puts "tight_layout()"
      fo.puts "savefig('#{fig_file}')"
    end

    # create_image_block raises following error:
    # asciidoctor/converter/html5.rb:552:in `image': undefined method `include?' for nil:NilClass (NoMethodError)
    create_image_block parent, file_name:fig_file, title:caption
  end

  # call this after parsing entire document
  # (renders plots)
  def at_end
    system("python #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end




If you reply to this email, your message will be added to the discussion below:
http://discuss.asciidoctor.org/create-image-block-tp6474.html
To start a new topic under Asciidoctor :: Discussion, email [hidden email]
To unsubscribe from Asciidoctor :: Discussion, click here.
NAML


--
Dan Allen | @mojavelinux | https://twitter.com/mojavelinux
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

ttmetro
@mojavelinux,

Thank you very much for the response. My code (below) works now.

The only part that does not is I don't get a Figure caption and number with the image. But I can live with that.

Bernhard


```
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage (see pyplot.adoc)
#
#    .Title of this pyplot
#    [pyplot]
#    ----
#    def f(t):
#      return cos(2 * pi * t) * exp(-t)
#    t = linspace(0, 5, 500)
#
#    plot(t, f(t))
#    title("Damped exponential decay")
#    text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
#    xlabel("Time [s]")
#    ylabel("Voltage [mV]")
#    ----
#
# then run
#    python3 pyplot.py
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl
  named :pyplot
  on_context :listing
  parse_content_as :simple

  PY_FILE = "%s/pyplot.py"
  PL_FILE = "%s/figures/py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    base_dir = parent.document.base_dir
    attributes = parent.document.attributes
    prefix     = attributes['pyplot_prefix']  || "clf();  figure(figsize=(5,4))"
    postfix    = attributes['pyplot_postfix'] || "tight_layout()"
    pyfile     = PY_FILE % base_dir

    if @@counter < 1
      # create PY_FILE with header
      File.open(pyfile, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
        fo.puts "import os"
        fo.puts "rc('text', usetex=True)"
        fo.puts "os.makedirs(os.path.dirname('#{PL_FILE}'), exist_ok=True)" % [ base_dir, 0 ]
      end
    end

    @@counter += 1
    attrs['target'] = PL_FILE % [ base_dir, @@counter ]
    attrs['numbered'] = true   # wish this would label and number captions but doesn't ...
    File.open(pyfile, 'a') do |fo|
      fo.puts
      fo.puts "%s #{attrs['target']}: #{attrs['title']}" % ('#'*5)
      fo.puts prefix
      fo.puts reader.source_lines
      fo.puts postfix
      fo.puts "savefig('#{attrs['target']}')"
    end
    # puts "attrs  = #{attrs}"
    create_image_block parent, attrs
  end

  # call this after parsing entire document
  # (renders plots)
  def at_end
    system("python3 #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end
```
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

mojavelinux
Administrator
That's great news!

If you want a caption with a number, you need to create the block, then set the title property then call the assign_caption method. This assignment is not currently handled by the create_image_block function.

img = create_image_block parent, { 'target' => 'foo.png' }
img.title = 'Your Caption Here'
img.assign_caption nil, 'figure'
img

It's a bit of a weird API, so perhaps it's something that could be improved. Feel free to open an issue.

Cheers,

-Dan

On Thu, Oct 11, 2018 at 6:31 PM ttmetro [via Asciidoctor :: Discussion] <[hidden email]> wrote:
@mojavelinux,

Thank you very much for the response. My code (below) works now.

The only part that does not is I don't get a Figure caption and number with the image. But I can live with that.

Bernhard


```
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage (see pyplot.adoc)
#
#    .Title of this pyplot
#    [pyplot]
#    ----
#    def f(t):
#      return cos(2 * pi * t) * exp(-t)
#    t = linspace(0, 5, 500)
#
#    plot(t, f(t))
#    title("Damped exponential decay")
#    text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
#    xlabel("Time [s]")
#    ylabel("Voltage [mV]")
#    ----
#
# then run
#    python3 pyplot.py
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl
  named :pyplot
  on_context :listing
  parse_content_as :simple

  PY_FILE = "%s/pyplot.py"
  PL_FILE = "%s/figures/py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    base_dir = parent.document.base_dir
    attributes = parent.document.attributes
    prefix     = attributes['pyplot_prefix']  || "clf();  figure(figsize=(5,4))"
    postfix    = attributes['pyplot_postfix'] || "tight_layout()"
    pyfile     = PY_FILE % base_dir

    if @@counter < 1
      # create PY_FILE with header
      File.open(pyfile, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
        fo.puts "import os"
        fo.puts "rc('text', usetex=True)"
        fo.puts "os.makedirs(os.path.dirname('#{PL_FILE}'), exist_ok=True)" % [ base_dir, 0 ]
      end
    end

    @@counter += 1
    attrs['target'] = PL_FILE % [ base_dir, @@counter ]
    attrs['numbered'] = true   # wish this would label and number captions but doesn't ...
    File.open(pyfile, 'a') do |fo|
      fo.puts
      fo.puts "%s #{attrs['target']}: #{attrs['title']}" % ('#'*5)
      fo.puts prefix
      fo.puts reader.source_lines
      fo.puts postfix
      fo.puts "savefig('#{attrs['target']}')"
    end
    # puts "attrs  = #{attrs}"
    create_image_block parent, attrs
  end

  # call this after parsing entire document
  # (renders plots)
  def at_end
    system("python3 #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end
```


If you reply to this email, your message will be added to the discussion below:
http://discuss.asciidoctor.org/create-image-block-tp6474p6512.html
To start a new topic under Asciidoctor :: Discussion, email [hidden email]
To unsubscribe from Asciidoctor :: Discussion, click here.
NAML


--
Dan Allen | @mojavelinux | https://twitter.com/mojavelinux
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

ttmetro
All works perfect now, thanks!
In the end, I prefer to run python manually when needed as it is much slower than document rendering. Below are the code and an example, in case someone else has a need.
Extension to convert inline python code to plots:

require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage (see pyplot.adoc)
#
#    .Title of this pyplot
#    [pyplot]
#    ----
#    def f(t):
#      return cos(2 * pi * t) * exp(-t)
#    t = linspace(0, 5, 500)
#
#    plot(t, f(t))
#    title("Damped exponential decay")
#    text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
#    xlabel("Time [s]")
#    ylabel("Voltage [mV]")
#    ----
#
# then run
#    python3 pyplot.py
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl
  named :pyplot
  on_context :listing
  parse_content_as :simple

  PY_FILE = "pyplot.py"
  PL_FILE = "figures/pyplot/py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    base_dir = parent.document.base_dir
    attributes = parent.document.attributes
    prefix     = attributes['pyplot_prefix']  || "clf();  figure(figsize=(5,4))"
    postfix    = attributes['pyplot_postfix'] || "tight_layout()"
    pyfile     = PY_FILE

    if @@counter < 1
      # create PY_FILE with header
      File.open(pyfile, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
        fo.puts "import os"
        fo.puts "rc('text', usetex=True)"
        fo.puts "os.makedirs(os.path.dirname('#{PL_FILE}'), exist_ok=True)" % 0
      end
    end

    @@counter += 1
    attrs['target'] = PL_FILE % @@counter
    attrs['numbered'] = true   # wish this would label and number captions but doesn't ...
    File.open(pyfile, 'a') do |fo|
      fo.puts
      fo.puts "%s #{attrs['target']}: #{attrs['title']}" % ('#'*5)
      fo.puts prefix
      fo.puts reader.source_lines
      fo.puts postfix
      fo.puts "savefig('#{attrs['target']}')"
    end
    # puts "attrs  = #{attrs}"
    img = create_image_block parent, attrs
    img.title = attrs['title']
    img.assign_caption nil, 'figure'
    img
  end

  # call this after parsing entire document
  # (renders plots)
  # Note: slow. Do this only when needed (from the command line).
  def at_end
    system("python3 #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end
Sample document:

= PyPlot test
:xrefstyle: short
:pyplot_prefix: clf();  figure(figsize=(5,4))
:pyplot_postfix: tight_layout()

Create plots from in-line python code.
Run this through asciidoctor

```
asciidoctor -r ~/Dropbox/Files/Lib/Adoc/ext/pyplot.rb
```

then create the figures with

```
python3 pyplot.py
```

<> is a python plot. <> is another.

[[exp_plot]]
.Plot of exponential decay showing off asciidoctor pyplot extension
[pyplot]
----
def f(t):
  return cos(2 * pi * t) * exp(-t)
t = linspace(0, 5, 500)

# Generate the plot with annotations
plot(t, f(t))
title("Damped exponential decay")
text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
xlabel("Time [s]")
ylabel("Voltage [mV]")
grid(True)
----

[[lin_plot]]
.Piecewise linear
[pyplot]
----
plot([3, 9, -2])
----
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

mojavelinux
Administrator
Super! Glad to help.

Btw, instead of running the pyplot.py in a separate step, you could use either a TreeProcessor or a Postprocessor to run it as part of the conversion. It would still run at the end, but it would be integrated into the asciidoctor command.

Cheers,

-Dan

On Fri, Oct 12, 2018 at 2:16 PM ttmetro [via Asciidoctor :: Discussion] <[hidden email]> wrote:
All works perfect now, thanks!
In the end, I prefer to run python manually when needed as it is much slower than document rendering. Below are the code and an example, in case someone else has a need.
Extension to convert inline python code to plots:

require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'

include Asciidoctor

# Inserts pyplot image.
#
# Usage
#
#   [pyplot]
#   ----
#   plot([1,2,3,4])
#   title('pyplot test')
#   ----
#
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'date'

include Asciidoctor

# Create pyplot
#
# Usage (see pyplot.adoc)
#
#    .Title of this pyplot
#    [pyplot]
#    ----
#    def f(t):
#      return cos(2 * pi * t) * exp(-t)
#    t = linspace(0, 5, 500)
#
#    plot(t, f(t))
#    title("Damped exponential decay")
#    text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
#    xlabel("Time [s]")
#    ylabel("Voltage [mV]")
#    ----
#
# then run
#    python3 pyplot.py
#
class PyplotBlock < Extensions::BlockProcessor

  use_dsl
  named :pyplot
  on_context :listing
  parse_content_as :simple

  PY_FILE = "pyplot.py"
  PL_FILE = "figures/pyplot/py_fig_%d.svg"
  @@counter = 0   # number of plot

  def process parent, reader, attrs

    base_dir = parent.document.base_dir
    attributes = parent.document.attributes
    prefix     = attributes['pyplot_prefix']  || "clf();  figure(figsize=(5,4))"
    postfix    = attributes['pyplot_postfix'] || "tight_layout()"
    pyfile     = PY_FILE

    if @@counter < 1
      # create PY_FILE with header
      File.open(pyfile, 'w') do |fo|
        fo.puts "\# machine generated by PyplotBlock on #{DateTime.now}"
        fo.puts "from pylab import *"
        fo.puts "import os"
        fo.puts "rc('text', usetex=True)"
        fo.puts "os.makedirs(os.path.dirname('#{PL_FILE}'), exist_ok=True)" % 0
      end
    end

    @@counter += 1
    attrs['target'] = PL_FILE % @@counter
    attrs['numbered'] = true   # wish this would label and number captions but doesn't ...
    File.open(pyfile, 'a') do |fo|
      fo.puts
      fo.puts "%s #{attrs['target']}: #{attrs['title']}" % ('#'*5)
      fo.puts prefix
      fo.puts reader.source_lines
      fo.puts postfix
      fo.puts "savefig('#{attrs['target']}')"
    end
    # puts "attrs  = #{attrs}"
    img = create_image_block parent, attrs
    img.title = attrs['title']
    img.assign_caption nil, 'figure'
    img
  end

  # call this after parsing entire document
  # (renders plots)
  # Note: slow. Do this only when needed (from the command line).
  def at_end
    system("python3 #{PY_FILE}")
  end
end

Asciidoctor::Extensions.register do
  block PyplotBlock
end
Sample document:

= PyPlot test
:xrefstyle: short
:pyplot_prefix: clf();  figure(figsize=(5,4))
:pyplot_postfix: tight_layout()

Create plots from in-line python code.
Run this through asciidoctor

```
asciidoctor -r ~/Dropbox/Files/Lib/Adoc/ext/pyplot.rb
```

then create the figures with

```
python3 pyplot.py
```

<> is a python plot. <> is another.

[[exp_plot]]
.Plot of exponential decay showing off asciidoctor pyplot extension
[pyplot]
----
def f(t):
  return cos(2 * pi * t) * exp(-t)
t = linspace(0, 5, 500)

# Generate the plot with annotations
plot(t, f(t))
title("Damped exponential decay")
text(3, 0.15, r"$y = \cos(2 \pi t) e^{-t}$")
xlabel("Time [s]")
ylabel("Voltage [mV]")
grid(True)
----

[[lin_plot]]
.Piecewise linear
[pyplot]
----
plot([3, 9, -2])
----



If you reply to this email, your message will be added to the discussion below:
http://discuss.asciidoctor.org/create-image-block-tp6474p6515.html
To start a new topic under Asciidoctor :: Discussion, email [hidden email]
To unsubscribe from Asciidoctor :: Discussion, click here.
NAML


--
Dan Allen | @mojavelinux | https://twitter.com/mojavelinux
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

ttmetro
That's what I wanted until I realized that the python step is much slower than formatting the adoc.

So I run python only occasionally when I've added a new plot, not to slow down the live view.

Thanks,
Bernhard
Reply | Threaded
Open this post in threaded view
|

Re: create_image_block

mojavelinux
Administrator
Bernhard,

Ahhhh. Now I see what you are saying. In that case, 👍.

Cheers,

-Dan

On Fri, Oct 12, 2018 at 4:56 PM ttmetro [via Asciidoctor :: Discussion] <[hidden email]> wrote:
That's what I wanted until I realized that the python step is much slower than formatting the adoc.

So I run python only occasionally when I've added a new plot, not to slow down the live view.

Thanks,
Bernhard


If you reply to this email, your message will be added to the discussion below:
http://discuss.asciidoctor.org/create-image-block-tp6474p6517.html
To start a new topic under Asciidoctor :: Discussion, email [hidden email]
To unsubscribe from Asciidoctor :: Discussion, click here.
NAML


--
Dan Allen | @mojavelinux | https://twitter.com/mojavelinux