Get Elevation Views PNG

I’ve been exporting two iso views as PNGs for quite some time. I first stored the camera position and then restored it at the end of the method.

For the elevations I tried using send action instead, but the pictures exported are not using the new view.

This (partial) code exports 4 images that are the same instead of four sides as anticipated.


 #front view
    cam.perspective = false
    Sketchup.send_action("viewFront:")
    Sketchup.send_action("viewZoomExtents:")
    options[:filename] = f3.path
    Sketchup.active_model.active_view.write_image(options)
    #left
    Sketchup.send_action("viewLeft:")
    Sketchup.send_action("viewZoomExtents:")
    options[:filename] = f4.path
    Sketchup.active_model.active_view.write_image(options)
    #back
    Sketchup.send_action("viewBack:")
    Sketchup.send_action("viewZoomExtents:")
    options[:filename] = f5.path
    Sketchup.active_model.active_view.write_image(options)
    #right
    Sketchup.send_action("viewRight:")
    Sketchup.send_action("viewZoomExtents:")
    options[:filename] = f6.path
    Sketchup.active_model.active_view.write_image(options)

    #restore camera position
    cam.perspective = perspective
    #Sketchup.send_action("viewIso:")
    @eye = eye
    @target = target
    @up = up
    #if I don't use a timer the position is not restored because apparently the 'send actions' happen after the method is done.
    UI.start_timer(0.0001) { Sketchup.active_model.active_view.camera.set(@eye, @target, @up) }

Is the only option to calculate the camera position manually?

OK I was able to set the camera position rather than using send_action.

    options = {
      :filename => f1.path,
      :width => 1200,
      :height => 600,
      :antialias => true,
      :compression => 1.0,
      :transparent => false
    }

    #front view
    cam.perspective = false
    center = Sketchup.active_model.bounds.center
    cam.set(center - Geom::Vector3d.new(0, 1000, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f3.path
    Sketchup.active_model.active_view.write_image(options)
    #left
    cam.set(center - Geom::Vector3d.new(1000, 0, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f4.path
    Sketchup.active_model.active_view.write_image(options)
    #back
    cam.set(center + Geom::Vector3d.new(0, 1000, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f5.path
    Sketchup.active_model.active_view.write_image(options)
    #right
    cam.set(center + Geom::Vector3d.new(1000, 0, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f6.path
    Sketchup.active_model.active_view.write_image(options)

    #restore camera position
    cam.perspective = perspective
    Sketchup.active_model.active_view.camera.set(eye, target, up)

This takes 3.5 seconds to write six images. Is there a faster way to generate png files? I’m base64 encoding them in the end and using them in a web request. Is there a way to keep from writing to a temp file?

Well, anti-aliasing will be slower. (Why use AA if not transparent ?)

And I think compression applies only to JPEG files. (At least this is what the docs say. Yes, I do notice that all 3 code examples have a compression option for a PNG write. Something to ask @tt_su about.)

SketchUp is using a compiled library for this, so I doubt using interpreted Ruby code could be faster.

If the elevation scene pages were setup ahead of time it might be slightly faster but I don’t know as the user would perceive it.


RELATED: Some ideas can be got from:

It should be termed as the “proper option” as send actions are meant for toolbar buttons not code. They are asynchronous, as you found out, and the following code statements run before the file actually gets written. (Likely the 4 files look like the final “right” view.)

The difference using AA or not is negligible compared to the difference in the results.
3.36 vs 3.86 seconds.

2021-06-19 18_03_47-Photos

The main problem is that the UI freezes for the duration of the export making for a poor user experience. If the images could be written async I wouldn’t care if it took a second or two longer.

And making the changes to the camera is virtually instant. 33ms according to my tests.

I really don’t understand what could possibly be taking so long to generate the images. SketchUp can generate more than 30 frames per second when orbiting.

It is the coder’s responsibility to keep the user informed about what is happening, via popup messagebox, notification balloon, etc.

However, the user could be changing the view in the meantime giving strange results.
This is the reason that file IO operations are always blocking.

It’s generating PNG image data from the view’s memory (bitmap) and then creating and writing file. IO operations take time. It will vary depending upon where the files are saved. (Ie, a slow physical drive, a fast solid state drive, a network path with high latency, etc.)

And then also the SketchUp API’s file IO tends to write to temp files and then copy them afterward. So it can be dependent on the speed of the user’s system disk (or where the TEMP folder is set to reside,) and the speed of the destination drive.

ADD: Just a note that the number of textures or colors may add to the overhead for some image formats. For example, in bitmaps there is a color collection near the top of the file with a color definition for every color used in the bitmap.

I do inform the user but it’s still a poor experience. I really can’t show progress because SketchUp is froze for the duration.

I thought it might be pretty quick to get an ‘internal snapshot’ and then do any processing in the background.

This doesn’t appear to be the bottleneck, considering the following code takes only 15ms.

    t = Time.now
    b1 = "data:image/png;base64," + Base64.encode64(File.open(f1, 'rb', &:read))
    b2 = "data:image/png;base64," + Base64.encode64(File.open(f2, 'rb', &:read))
    b3 = "data:image/png;base64," + Base64.encode64(File.open(f3, 'rb', &:read))
    b4 = "data:image/png;base64," + Base64.encode64(File.open(f4, 'rb', &:read))
    b5 = "data:image/png;base64," + Base64.encode64(File.open(f5, 'rb', &:read))
    b6 = "data:image/png;base64," + Base64.encode64(File.open(f6, 'rb', &:read))
    f1 = Tempfile.new(['view1','.txt'])
    f2 = Tempfile.new(['view2','.txt'])
    f3 = Tempfile.new(['view3','.txt'])
    f4 = Tempfile.new(['view4','.txt'])
    f5 = Tempfile.new(['view5','.txt'])
    f6 = Tempfile.new(['view6','.txt'])
    File.open(f1, 'w') { |file| file.write(b1) }
    File.open(f2, 'w') { |file| file.write(b2) }
    File.open(f3, 'w') { |file| file.write(b3) }
    File.open(f4, 'w') { |file| file.write(b4) }
    File.open(f5, 'w') { |file| file.write(b5) }
    File.open(f6, 'w') { |file| file.write(b6) }
    puts "#{Time.now - t} seconds to convert to base64 and write back to disk."

#> 0.015007 seconds to convert to base64 and write back to disk.

#view.write_image is slow, and can take well over 1 second depending on the complexity of your model and the resolution. I guess that what eats the time is the conversion of the frame buffer into a PNG record and matching the required resolution. This a main issue I have with Animator.

You can speed up the process by using :source => :framebuffer in the parameter of write_image. This means that you take the resolution of the screen.

On my 4K screen that takes 13 seconds instead of 3.86, and results ins a file about 20x the size.
If I zoom extents first it takes 25 seconds. :frowning:

:thinking:

You did not previously mention you had SketchUp running on a 4K screen.
IMO, SketchUp does not support 4K and 5K resolutions well.

You might have to look into using a 3rd party library like ImageMagick, FreeImage, etc.
Some of these 3rd party libs do have Ruby bindings.

I wouldn’t know where to start… :frowning:

Most likely here …

I just installed ImageMagick 7.1.0 and checked the box for legacy utilities because you’d need the convert utility.

After install and reloading SketchUp (because the IM path is added to the ENV["PATH"] list and SketchUp (and any other application) makes a copy of the environment when it loads.

So anyway this simple statement does work in SketchUp Ruby on Windows:

%x{magick convert "screenshot:[0]" "screenshot.png"}

The index is the display number. 0 is my primary display.
There is also a cropping parameter that we’d need to figure out. (Done - see example below)

The drawback to using %x on Windows is it pops up the blank console window for smidgen of a second.
We usually try to use WIN32OLE and the Windows Scripting Host’s Run or Exec methods to prevent the window.

This works for me. (Note I had to hardcode the x and y as view.corner(0) always returns [0,0].

module Viewport

  WIN = Sketchup.platform == :platform_win
  require 'win32ole' if WIN

  extend self

  def get_cmd(view,filename,type)
    # view.corner(0) is top left in view space: ALWAYS [0,0]
    # x, y = view.corner(0) # NO HELP !
    x, y = 100, 120
    h = view.vpheight
    w = view.vpwidth
    # On Windows, there are several convert.exe commands, all of which
    # are in the PATH. So you must specify the path to the right convert.exe
    # executable. Solution: Add "magick" before "convert" in command string.
    if WIN
      # Note, use a double quote (") rather than a single quote (')
      # for the ImageMagick command line under Windows:
      %(magick convert "screenshot:[0]" -crop "#{w}x#{h}+#{x}+#{y}" "#{filename}.#{type}")
    else
      %(convert 'screenshot:[0]' -crop '#{w}x#{h}+#{x}+#{y}' '#{filename}.#{type}')
    end
  end

  def save_viewport(filename = nil, type = "png", path = Dir.pwd)
    model = Sketchup.active_model
    if filename.nil?
      filename = model.title
      filename = "sketchup_view" if filename.empty?
    end
    view = model.active_view
    cmd = get_cmd(view,filename,type)
    if $VERBOSE
      puts "Executing shell command:"
      puts cmd
      puts "Return code 0 means nothing:"
    end
    Dir.chdir(path) do
      if WIN
        # Use OLE Windows Scripting Host Shell,
        # minmize any window, do not change focus:
        WIN32OLE.new("Wscript.Shell").Run(cmd,7)
      else
        # Mac: ( also WORKS on Windows, BUT shows blank shell Window)
        %x[#{cmd}]
      end
    end
  end

end

(scroll to see all code)

This (above) is just to see that it works. Not that it works fast.
For speed one would need to do a compiled Ruby C extension that uses the libraries directly rather than slow IO passing command strings to shell subprocesses.

Also it (the example) is not efficient as the method creates a local shell object each time it runs.
This could be created in a wrapping method and passed to the “save” method as a argument.


Also @Aerilius has done some work wrapping the ImageMagick command line tool:

1 Like

It applies to PNG as well. Any format that perform compression.

I’m curious, have you tried writing out BMP files? Comparing that against PNG/JPEG might give us a clue to whether it’s the rendering or file-format compression being the bottleneck.

Also, have you compared using AA vs not using AA?

Yes

BMP with AA = 3.41 seconds
BMP without AA = 2.99 seconds.

It seems to me that the rendering is the bottleneck.

1 Like

Here is the model and the code if you want to do your own testing.

Demo.skp (1.1 MB)

Ruby Code
  def int
    center = Sketchup.active_model.bounds.center
    l = Sketchup.active_model.bounds.height
    w = Sketchup.active_model.bounds.width
    h = Sketchup.active_model.bounds.depth
    c  = [center[0] - (w*0.1),center[1] - (l*0.1),center[2]]
    c1 = [center[0] - (w*0.1),center[1] + (l*0.1),center[2]]
    c2 = [center[0] + (w*0.1),center[1] + (l*0.1),center[2]]
    c3 = [center[0] + (w*0.1),center[1] - (l*0.1),center[2]]
    m = Sketchup.active_model.bounds.min
    Sketchup.active_model.active_view.field_of_view = 35
    @l = [[m[0]-(l*0.8),m[1]-(w*0.8),h/2], c,[0,0,1]]
    @l1 = [[m[0]-(l*0.8),m[1] + l +(w*0.8),h/2], c1,[0,0,1]]
    @r = [[m[0] + w +(l*0.8),m[1] + l +(w*0.8),h/2], c2,[0,0,1] ]
    @r1 = [[m[0] + w +(l*0.8),m[1]-(w*0.8),h/2], c3,[0,0,1] ]
    @c = Sketchup.active_model.active_view.camera
  end

  def set_zoom
    Sketchup.active_model.active_view.zoom_extents
    Sketchup.active_model.active_view.zoom 0.9
  end

  def vl
    int
    @c.set @l[0],@l[1],@l[2]
    set_zoom
  end

  def vl1
    int
    @c.set @l1[0],@l1[1],@l1[2]
    set_zoom
  end

  def vr
    int
    @c.set @r[0],@r[1],@r[2]
    set_zoom
  end

  def vr1
    int
    @c.set @r1[0],@r1[1],@r1[2]
    set_zoom
  end

  def generate_views_base64(v)
    require 'base64'
    view = Sketchup.active_model.active_view
    cam = Sketchup.active_model.active_view.camera
    eye = cam.eye
    target = cam.target
    up = cam.up
    perspective = cam.perspective?
    file_extension = '.png'
    f1 = Tempfile.new(['view1', file_extension])
    f2 = Tempfile.new(['view2', file_extension])
    f3 = Tempfile.new(['view3', file_extension])
    f4 = Tempfile.new(['view4', file_extension])
    f5 = Tempfile.new(['view5', file_extension])
    f6 = Tempfile.new(['view6', file_extension])
    if v == 1 then vr else vr1 end
    options = {
      :filename => f1.path,
      :width => 1200,
      :height => 600,
      :antialias => true,
      :compression => 1.0,
      :transparent => false
    }
    #write iso views
    Sketchup.active_model.active_view.write_image(options)
    if v == 1 then vl else vl1 end
    options[:filename] = f2.path
    Sketchup.active_model.active_view.write_image(options)

    #front view
    cam.perspective = false
    center = Sketchup.active_model.bounds.center
    cam.set(center - Geom::Vector3d.new(0, 1000, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f3.path
    Sketchup.active_model.active_view.write_image(options)
    #left
    cam.set(center - Geom::Vector3d.new(1000, 0, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f4.path
    Sketchup.active_model.active_view.write_image(options)
    #back
    cam.set(center + Geom::Vector3d.new(0, 1000, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f5.path
    Sketchup.active_model.active_view.write_image(options)
    #right
    cam.set(center + Geom::Vector3d.new(1000, 0, 0), center, [0,0,1])
    view.zoom_extents
    options[:filename] = f6.path
    Sketchup.active_model.active_view.write_image(options)

    #restore camera position
    cam.perspective = perspective
    Sketchup.active_model.active_view.camera.set(eye, target, up)
    b1 = "data:image/png;base64," + Base64.encode64(File.open(f1, 'rb', &:read))
    b2 = "data:image/png;base64," + Base64.encode64(File.open(f2, 'rb', &:read))
    b3 = "data:image/png;base64," + Base64.encode64(File.open(f3, 'rb', &:read))
    b4 = "data:image/png;base64," + Base64.encode64(File.open(f4, 'rb', &:read))
    b5 = "data:image/png;base64," + Base64.encode64(File.open(f5, 'rb', &:read))
    b6 = "data:image/png;base64," + Base64.encode64(File.open(f6, 'rb', &:read))
    [b1, b2, b3, b4, b5, b6]
  end

  def do_test
    t = Time.now
    generate_views_base64(0)
    puts "#{Time.now - t} seconds to generate views."
  end
1 Like