[Ipe-discuss] Plot data extraction from PDF
Jan Hlavacek
jhlavace at svsu.edu
Mon Dec 28 17:02:16 CET 2009
On Mon, Dec 28, 2009 at 03:49:06PM +0000, T T wrote:
> Hi,
>
> I'm in need of a tool that can extract XY data from (vector!) plots in
> PDF documents. Failing to find such a tool I'm considering writing one
> and Ipe looks attractive, because:
> - it can read PDF files (with pdftoipe tool)
> - it is scriptable with Lua and I happen to know this language a little bit
>
> As I'm unfamiliar with implementation and object hierarchy of Ipe, I
> would appreciate some guidance in how to implement a plug-in with the
> following methods:
> (1) Coordinate system: establish the plot coordinate system from two
> selected marks (if not given use canvas coordinate system)
This will probably not help you with the rest, but my ipeplots ipelet does
something like this. Given a selection, it establishes a coordinate system
from the bounding box of the selection, if there is no selection, it uses the
canvas coordinate system. I have a new version written, right now I am in the
process of writing documentation. I will attach the ipelet to this file, so
you can have a look at it. Hopefully it will be of help.
--
Jan Hlavacek (jhlavace at svsu.edu, (989) 964-2004)
Department of Mathematical Sciences, Saginaw Valley State University
http://www.svsu.edu/~jhlavace/
-------------- next part --------------
----------------------------------------------------------------------
-- plot ipelet
----------------------------------------------------------------------
--[[
This file is an extension of the drawing editor Ipe (ipe7.sourceforge.net)
Copyright (c) 2009 Jan Hlavacek
This file can be distributed and modified under the terms of the GNU General
Public License as published by the Free Software Foundation; either version
3, or (at your option) any later version.
This file is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You can find a copy of the GNU General Public License at
"http://www.gnu.org/copyleft/gpl.html", or write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
Basic documentation (bug: better documentation needs to be written)
All functions provided by this ipelet use a selection to describe the plot
"viewport" on the page. The only thing that is used from the selection is
its bounding rectangle. This rectangle will represent the "viewport" of the
plot on your page. Each function will present a dialog where you, in
addition to other things, specify the corresponding plot coordinates for
this viewport.
For example, assume that you start with a selection that is a rectangle.
You choose Parametric plot, type in cos(t) and sin(t) for x and y, t from
-3.14 to 3.14, and viewport coordinates as x from -1 to 1, y from -1 to 1.
This will make the originally selected rectangle represent a part of
coordinate plane with corners (-1,-1), (-1,1), (1,1) and (1,-1), and, in
effect, draw an inscribed ellipse into the rectangle.
There is no clipping going on right now, so if your plot is larger than the
specified viewport, it will simply stick out of the rectangle.
After creating a coordinate system with the "Coordinate system" menu item,
you can use this coordinate system as your initial selection for your plots,
and they will be correctly placed on this coordinate system.
When creating coordinate system, you can specify location of ticks on both
axes. Here you can use expressions such as pi, 2*pi, etc. If you leave
this empty, ticks will be placed at every integer. Ticks will only be drawn
if the tick size is not 0.
I am planning to write a more expensive documentation for this. In the mean
time, if you have questions, please contact me at jhlavace at svsu.edu. Also
please contact me if you have any suggestions for improvement.
This is my first attempt to write something in lua, and it has been put
together quite in a hurry, so I am sure there are lot of places where things
can be done better.
Future plans: Add coordinate grid generating function, make general lua
math expressions work at other places than just tick lists (so you don't
have to enter things like 3.14 for t), add polar plots, ...
--]]
label = "Plots"
about = [[
Parametric curves, plots of functions, coordinate systems
]]
-- we will prepend this every time we use loadstring, so user does not have to
-- type math.foo for foo all the time:
local mathdefs = "local abs = math.abs; local acos = math.acos; local asin = math.asin; local atan = math.atan; local atan2 = math.atan2; local ceil = math.ceil; local cos = math.cos; local cosh = math.cosh; local deg = math.deg; local exp = math.exp; local floor = math.floor; local fmod = math.fmod; local log = math.log; local log10 = math.log10; local max = math.max; local min = math.min; local modf = math.modf; local pi = math.pi; local pow = math.pow; local rad = math.rad; local sin = math.sin; local sinh = math.sinh; local sqrt = math.sqrt; local tan = math.tan; local tanh = math.tanh;"
local function isfinite(x)
return (x > - math.huge) and (x < math.huge)
end
local function bounding_box(p)
local box = ipe.Rect()
for i,obj,sel,layer in p:objects() do
if sel then box:add(p:bbox(i)) end
end
return box
end
local function calculate_transform (model, x0, y0, x1, y1)
local box = bounding_box(model:page())
if box:isEmpty() then
if model.snap.with_axes then
--ui:explain("Selection seems to be empty. Using coordinates with origin.")
return ipe.Translation(model.snap.origin)
else
--ui:explain("Selection seems to be empty. Using global coordinates.")
return ipe.Matrix()
end
end
-- Selection is given. Calculate transformation to change real coordinates
-- to canvas coordinates relative to the selection.
local cstart = box:bottomLeft()
local cend = box:topRight()
local cdif = cend-cstart
local cxlen = cdif.x
local cylen = cdif.y
local start = ipe.Vector(x0,y0)
local xlen = x1-x0
local ylen = y1-y0
local scalem = ipe.Matrix(cxlen/xlen,0,0,cylen/ylen)
local trans = ipe.Translation(cstart-scalem*start) * scalem
return trans
end
local function get_number (model, string, error_msg)
if string == "" then
model:warning ("You need to specify " .. error_msg)
return
end
lstring = mathdefs .. "return " .. string
local f,err = _G.loadstring(lstring,error_msg)
if not f then
model:warning("Could not compile " .. error_msg)
return
end
local stat,num = _G.pcall(f)
if not stat then
model:warning(num) -- bug: error messages will be cryptic
return
end
if not num then
model:warning(string .. " is not a valid value for " .. error_msg)
return
end
return num
end
-- helpful functions for creating dialogs
-- Set up a line counter so that we don't have to use absolute line numbers for
-- dialogs.
function line_counter()
local line_no = 0
local function same_line()
return line_no
end
local function new_line()
line_no = line_no + 1
return line_no
end
return same_line, new_line
end
-- parametric plot
function curve(model)
local box = bounding_box(model:page())
local has_viewport = not box:isEmpty()
local same, nxt = line_counter()
local d = ipeui.Dialog(model.ui, "Parametric plot")
d:add("label1", "label", {label="Enter parametric equations. Use t as a parameter."}, nxt(), 1, 1, 4)
d:add("label2", "label", {label="x="}, nxt(), 1)
d:add("xeq", "input", {}, same(), 2, 1, 3)
d:add("label3", "label", {label="y="}, nxt(), 1)
d:add("yeq", "input", {}, same(), 2, 1, 3)
d:add("label4", "label", {label="Set the domain for t:"}, nxt(), 1, 1, 4)
d:add("label5", "label", {label="from:"}, nxt(), 1, 1, 1)
d:add("tfrom", "input", {}, same(), 2, 1, 1)
d:add("label6", "label", {label="to:"}, same(), 3, 1, 1)
d:add("tto", "input", {}, same(), 4, 1, 1)
if has_viewport then
d:add("label7", "label", {label="Set coordinates for viewport:"}, nxt(), 1, 1, 4)
d:add("label8", "label", {label="from x="}, nxt(), 1, 1, 1)
d:add("xfrom", "input", {}, same(), 2, 1, 1)
d:add("label9", "label", {label="to x="}, same(), 3, 1, 1)
d:add("xto", "input", {}, same(), 4, 1, 1)
d:add("label10", "label", {label="from y="}, nxt(), 1, 1, 1)
d:add("yfrom", "input", {}, same(), 2, 1, 1)
d:add("label11", "label", {label="to y="}, 8, 3, 1, 1)
d:add("yto", "input", {}, same(), 4, 1, 1)
end
d:add("label12", "label", {label="number of points"}, nxt(), 1, 1, 1)
d:add("points", "input", {}, same(), 2, 1, 1)
d:add("ok", "button", {label="&Ok", action="accept"}, nxt(), 4)
d:add("cancel", "button", {label="&Cancel", action="reject"}, same(), 3)
d:setStretch("column", 2, 1)
d:setStretch("row", 5, 1)
if xeqstore then d:set("xeq",xeqstore) end
if yeqstore then d:set("yeq",yeqstore) end
if has_viewport then
if x0store then d:set("xfrom",x0store) end
if x1store then d:set("xto",x1store) end
if y0store then d:set("yfrom",y0store) end
if y1store then d:set("yto",y1store) end
end
if t0store then d:set("tfrom",t0store) end
if t1store then d:set("tto",t1store) end
if not pointsstore then pointsstore = 100 end
d:set("points",pointsstore)
if not d:execute() then return end
local s1 = d:get("xeq")
local s2 = d:get("yeq")
xeqstore = s1
yeqstore = s2
if has_viewport then
x0store = d:get("xfrom")
x1store = d:get("xto")
y0store = d:get("yfrom")
y1store = d:get("yto")
end
t0store = d:get("tfrom")
t1store = d:get("tto")
pointsstore = d:get("points")
-- real coordinates
local x0, x1, y0, y1
if has_viewport then
x0 = get_number(model,x0store,"lower x limit")
if not x0 then return end
x1 = get_number(model,x1store,"upper x limit")
if not x1 then return end
y0 = get_number(model,y0store,"lower y limit")
if not y0 then return end
y1 = get_number(model,y1store,"upper y limit")
if not y1 then return end
else
x0 = 0
y0 = 0
x1 = 1
y1 = 1
end
-- parameter
local t0 = get_number(model,t0store,"initial value of t")
if not t0 then return end
local t1 = get_number(model,t1store,"final value of t")
if not t0 then return end
-- check validity of t limits:
if t0 > t1 then
t0, t1 = t1, t0
end
if t0 == t1 then
model:warning("Limits for t cannot be equal")
return
end
-- check validity of x and y limits:
if x0 > x1 then
x0, x1 = x1, x0
end
if x0 == x1 then
model:warning("Limits for x cannot be equal")
return
end
if y0 > y1 then
y0, y1 = y1, y0
end
if y0 == y1 then
model:warning("Limits for y cannot be equal")
return
end
local trans = calculate_transform(model,x0,y0,x1,y1)
local tlen = t1-t0
local t = t0
-- create user function
local coordstr = s1 .. "," .. s2
coordstr = mathdefs .. "return function (t) local v = ipe.Vector(" .. coordstr .. "); return v end"
local f,err = _G.loadstring(coordstr,"parametric_plot")
if not f then
model:warning("Could not compile coordinate functions")
return
end
local curve = { type="curve", closed=false }
local v0 = trans*f()(t)
local v1 = v0
for i = 1,pointsstore do
t = t + tlen/pointsstore
v1 = trans*f()(t)
curve[#curve + 1] = { type="segment", v0, v1 }
v0 = v1
end
local graph = ipe.Path(model.attributes, { curve } )
model:creation("create graph", graph)
end
-- plot of a function:
function func_plot(model)
local box = bounding_box(model:page())
local has_viewport = not box:isEmpty()
local same, nxt = line_counter()
local d = ipeui.Dialog(model.ui, "Function Plot")
d:add("label1", "label", {label="Enter y as a function of x"}, nxt(), 1, 1, 4)
d:add("label2", "label", {label="y="}, nxt(), 1)
d:add("xeq", "input", {}, same(), 2, 1, 3)
d:add("label4", "label", {label="Set the domain for x:"}, nxt(), 1, 1, 4)
d:add("label5", "label", {label="from:"}, nxt(), 1, 1, 1)
d:add("tfrom", "input", {}, same(), 2, 1, 1)
d:add("label6", "label", {label="to:"}, same(), 3, 1, 1)
d:add("tto", "input", {}, same(), 4, 1, 1)
if has_viewport then
d:add("label7", "label", {label="Set coordinates for viewport:"}, nxt(), 1, 1, 4)
d:add("label8", "label", {label="from x="}, nxt(), 1, 1, 1)
d:add("xfrom", "input", {}, same(), 2, 1, 1)
d:add("label9", "label", {label="to x="}, same(), 3, 1, 1)
d:add("xto", "input", {}, same(), 4, 1, 1)
d:add("label10", "label", {label="from y="}, nxt(), 1, 1, 1)
d:add("yfrom", "input", {}, same(), 2, 1, 1)
d:add("label11", "label", {label="to y="}, same(), 3, 1, 1)
d:add("yto", "input", {}, same(), 4, 1, 1)
end
d:add("label12", "label", {label="number of points"}, nxt(), 1, 1, 1)
d:add("points", "input", {}, same(), 2, 1, 1)
d:add("ok", "button", {label="&Ok", action="accept"}, nxt(), 4)
d:add("cancel", "button", {label="&Cancel", action="reject"}, same(), 3)
d:setStretch("column", 2, 1)
d:setStretch("row", 5, 1)
if fstore then d:set("xeq",fstore) end
if has_viewport then
if x0store then d:set("xfrom",x0store) end
if x1store then d:set("xto",x1store) end
if y0store then d:set("yfrom",y0store) end
if y1store then d:set("yto",y1store) end
end
if dom0store then d:set("tfrom",dom0store) end
if dom1store then d:set("tto",dom1store) end
if not pointsstore then pointsstore = 100 end
d:set("points",pointsstore)
if not d:execute() then return end
local s1 = d:get("xeq")
fstore = s1
if has_viewport then
x0store = d:get("xfrom")
x1store = d:get("xto")
y0store = d:get("yfrom")
y1store = d:get("yto")
end
dom0store = d:get("tfrom")
dom1store = d:get("tto")
pointsstore = d:get("points")
-- real coordinates
local x0, x1, y0, y1
if has_viewport then
x0 = get_number(model,x0store,"lower x limit")
if not x0 then return end
x1 = get_number(model,x1store,"upper x limit")
if not x1 then return end
y0 = get_number(model,y0store,"lower y limit")
if not y0 then return end
y1 = get_number(model,y1store,"upper y limit")
if not y1 then return end
else
x0=0
y0=0
x1=1
y1=1
end
-- independent variable
local t0 = get_number(model,dom0store,"initial value of x")
if not t0 then return end
local t1 = get_number(model,dom1store,"final value of x")
if not t0 then return end
-- check validity of t limits:
if t0 > t1 then
t0, t1 = t1, t0
end
if t0 == t1 then
model:warning("Limits for x cannot be equal")
return
end
-- check validity of x and y limits:
if x0 > x1 then
x0, x1 = x1, x0
end
if x0 == x1 then
model:warning("Limits for x cannot be equal")
return
end
if y0 > y1 then
y0, y1 = y1, y0
end
if y0 == y1 then
model:warning("Limits for y cannot be equal")
return
end
-- scaling calculations
local trans = calculate_transform(model,x0,y0,x1,y1)
local tlen = t1-t0
local t = t0
-- create user function
local coordstr = s1
coordstr = mathdefs .. "return function (x) local v = ipe.Vector(x," .. coordstr .. "); return v end"
-- attempt to load this string. Give a warning and quit if it fails.
local f,err = _G.loadstring(coordstr,"function plot")
if not f then
model:warning(err) -- bug: error messages will be cryptic
return
end
-- execute the function obtained from the string. That should create the
-- actual function usable for our calculations. Warn and quit if it fails.
stat,f = _G.pcall(f)
if not stat then
model:warning(f) -- bug: error messages will be cryptic
return
end
local curve = { type="curve", closed=false }
local v0
-- try to evaluate the function. Warn and quit if it fails.
stat, v0 = _G.pcall(f,t)
if not stat then
model:warning(v0) -- bug: error messages will be cryptic
return
end
if not isfinite(v0.x*v0.y) then
model:warning("domain error")
return
end
v0 = trans*v0
local v1 = v0
for i = 1,pointsstore do
t = t + tlen/pointsstore
stat, v1 = _G.pcall(f,t)
if not stat then
model:warning(v1) -- bug: error messages will be cryptic
return
end
if not isfinite(v1.x*v1.y) then
model:warning("domain error")
return
end
v1 = trans*v1
curve[#curve + 1] = { type="segment", v0, v1 }
v0 = v1
end
local graph = ipe.Path(model.attributes, { curve } )
model:creation("create graph", graph)
end
-- coordinate system
function make_axes(model, num)
same, nxt = line_counter()
local d = ipeui.Dialog(model.ui, "Coordinate System")
d:add("label3", "label", {label="Set coordinates for viewport:"}, nxt(), 1, 1, 4)
d:add("label8", "label", {label="from x="}, nxt(), 1, 1, 1)
d:add("xfrom", "input", {}, same(), 2, 1, 1)
d:add("label9", "label", {label="to x="}, same(), 3, 1, 1)
d:add("xto", "input", {}, same(), 4, 1, 1)
d:add("label10", "label", {label="from y="}, nxt(), 1, 1, 1)
d:add("yfrom", "input", {}, same(), 2, 1, 1)
d:add("label11", "label", {label="to y="}, same(), 3, 1, 1)
d:add("yto", "input", {}, same(), 4, 1, 1)
if num == 1 then
d:add("label27", "label", {label="Size of x-ticks (in pt):"}, nxt(), 1, 1, 1)
d:add("xticksize", "input", {}, same(), 2, 1, 1)
d:add("label28", "label", {label="Size of y-ticks (in pt):"}, same(), 3, 1, 1)
d:add("yticksize", "input", {}, same(), 4, 1, 1)
d:add("label84", "label", {label="Locations of x-ticks:"},nxt(),1,1,1)
d:add("xticklist", "input", {}, same(), 2, 1, 3)
d:add("label85", "label", {label="Locations of y-ticks:"},nxt(),1,1,1)
d:add("yticklist", "input", {}, same(), 2, 1, 3)
else
d:add("label84", "label", {label="Locations of vertical grid lines:"},nxt(),1,1,1)
d:add("xticklist", "input", {}, same(), 2, 1, 3)
d:add("label85", "label", {label="Locations of horizontal grid lines:"},nxt(),1,1,1)
d:add("yticklist", "input", {}, same(), 2, 1, 3)
end
d:add("ok", "button", {label="&Ok", action="accept"}, nxt(), 4)
d:add("cancel", "button", {label="&Cancel", action="reject"}, same(), 3)
d:setStretch("column", 2, 1)
d:setStretch("row", 5, 1)
if x0store then d:set("xfrom",x0store) end
if x1store then d:set("xto",x1store) end
if y0store then d:set("yfrom",y0store) end
if y1store then d:set("yto",y1store) end
if not xticksizestore then xticksizestore = 0 end
if not yticksizestore then yticksizestore = 0 end
if (num == 1) then
d:set("xticksize", xticksizestore)
d:set("yticksize", yticksizestore)
end
if xtickstore then d:set("xticklist", xtickstore) end
if ytickstore then d:set("yticklist", ytickstore) end
if not d:execute() then return end
x0store = d:get("xfrom")
x1store = d:get("xto")
y0store = d:get("yfrom")
y1store = d:get("yto")
if num == 1 then
xticksizestore = d:get("xticksize")
yticksizestore = d:get("yticksize")
end
xtickstore = d:get("xticklist")
ytickstore = d:get("yticklist")
-- real coordinates
local x0 = get_number(model,x0store,"lower x limit")
if not x0 then return end
local x1 = get_number(model,x1store,"upper x limit")
if not x1 then return end
local y0 = get_number(model,y0store,"lower y limit")
if not y0 then return end
local y1 = get_number(model,y1store,"upper y limit")
if not y1 then return end
-- check validity of x and y limits:
if x0 > x1 then
x0, x1 = x1, x0
end
if x0 == x1 then
model:warning("Limits for x cannot be equal")
return
end
if y0 > y1 then
y0, y1 = y1, y0
end
if y0 == y1 then
model:warning("Limits for y cannot be equal")
return
end
local trans = calculate_transform(model,x0,y0,x1,y1)
-- ticks:
xticksize = tonumber(xticksizestore)
if not xticksize then xticksize = 0 end
yticksize = tonumber(yticksizestore)
if not yticksize then yticksize = 0 end
-- tick locations:
local xticks = {}
local yticks = {}
if xtickstore and (xtickstore ~= "") then
local tickliststr = mathdefs .. "return {" .. xtickstore .. "}"
-- attempt to load this string. Give a warning and quit if it fails.
local f,err = _G.loadstring(tickliststr,"x-ticks")
if not f then
model:warning(err) -- bug: error messages will be cryptic
return
end
local xticklist
stat, xticklist = _G.pcall(f,t)
if not stat then
model:warning(xticklist) -- bug: error messages will be cryptic
return
end
for i,x in pairs(xticklist) do
if isfinite(x) then
if (x > x0) and (x < x1) then
xticks[#xticks + 1] = x
end
end
end
else -- place ticks at every integer
for i = math.floor(x0) + 1, math.ceil(x1)-1 do
xticks[#xticks + 1] = i
end
end
-- do the same for y-ticks
if ytickstore and (ytickstore ~= "") then
local tickliststr = mathdefs .. "return {" .. ytickstore .. "}"
-- attempt to load this string. Give a warning and quit if it fails.
local f,err = _G.loadstring(tickliststr,"y-ticks")
if not f then
model:warning(err) -- bug: error messages will be cryptic
return
end
local yticklist
stat, yticklist = _G.pcall(f,t)
if not stat then
model:warning(yticklist) -- bug: error messages will be cryptic
return
end
for i,y in pairs(yticklist) do
if isfinite(y) then
if (y > y0) and (y < y1) then
yticks[#yticks + 1] = y
end
end
end
else -- place ticks at every integer
for i = math.floor(y0) + 1, math.ceil(y1)-1 do
yticks[#yticks + 1] = i
end
end
if (num == 1) then
local axes = { }
-- only make x-axis if y0<=0<=y1
if y0*y1 <= 0 then
local v0 = trans*ipe.Vector(x0,0)
local v1 = trans*ipe.Vector(x1,0)
local curve = { type="curve", closed=false; { type="segment", v0, v1 }}
local xaxis = ipe.Path(model.attributes, {curve})
xaxis:set("farrow",true)
xaxis:set("pen","fat")
axes[#axes + 1] = xaxis
if xticksize ~= 0 then
local half_tick = ipe.Vector(0,xticksize/2)
for n,i in pairs(xticks) do
v0 = trans*ipe.Vector(i,0)
local tick = { type="curve", closed=false; { type="segment", v0+half_tick, v0-half_tick }}
axes[#axes + 1] = ipe.Path(model.attributes, {tick})
end
end
end
if x0*x1 <= 0 then
local v0 = trans*ipe.Vector(0,y0)
local v1 = trans*ipe.Vector(0,y1)
curve = { type="curve", closed=false; { type="segment", v0, v1 }}
local yaxis = ipe.Path(model.attributes, {curve})
yaxis:set("farrow",true)
yaxis:set("pen","fat")
axes[#axes + 1] = yaxis
if yticksize ~= 0 then
local half_tick = ipe.Vector(yticksize/2,0)
for n,i in pairs(yticks) do
v0 = trans*ipe.Vector(0,i)
local tick = { type="curve", closed=false; { type="segment", v0+half_tick, v0-half_tick }}
axes[#axes + 1] = ipe.Path(model.attributes, {tick})
end
end
end
if #axes > 0 then
local coordsys = ipe.Group(axes)
model:creation("create coordinate system", coordsys)
end
else
local grid = {}
for n,i in pairs(xticks) do
local v0 = trans*ipe.Vector(i,y0)
local v1 = trans*ipe.Vector(i,y1)
local line = { type="curve", closed=false; { type="segment", v0, v1 }}
grid[#grid + 1] = ipe.Path(model.attributes, {line})
end
for n,i in pairs(yticks) do
local v0 = trans*ipe.Vector(x0,i)
local v1 = trans*ipe.Vector(x1,i)
local line = { type="curve", closed=false; { type="segment", v0, v1 }}
grid[#grid + 1] = ipe.Path(model.attributes, {line})
end
if #grid > 0 then
local coordsys = ipe.Group(grid)
model:creation("create coordinate grid", coordsys)
end
end
end
methods = {
{ label = "Coordinate system", run=make_axes },
{ label = "Coordinate grid", run=make_axes },
{ label = "Parametric plot", run=curve },
{ label = "Function plot", run=func_plot },
}
----------------------------------------------------------------------
More information about the Ipe-discuss
mailing list