summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua')
-rwxr-xr-xMaster/texmf-dist/scripts/extractbb/extractbb-scratch.lua695
1 files changed, 695 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua b/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua
new file mode 100755
index 00000000000..3b1cd6948be
--- /dev/null
+++ b/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua
@@ -0,0 +1,695 @@
+#!/usr/bin/env texlua
+-- extractbb-lua
+-- https://github.com/gucci-on-fleek/extractbb
+-- SPDX-License-Identifier: MPL-2.0+
+-- SPDX-FileCopyrightText: 2024 Max Chernoff
+--
+-- Inclusion Methods
+-- =================
+--
+-- This script can use two different methods to extract bounding boxes from
+-- images: the "img" module and the "pdfe" module. The "img" module will be
+-- automatically selected in most cases and supports all image types that are
+-- supported by the original "extractbb" program. If and only if the "img"
+-- module fails to load, the "pdfe" module will be used as a fallback. However,
+-- the "pdfe" module only supports PDF files. Both modules are built in to the
+-- LuaTeX binaries, however due to some technical issues, the "img" module may
+-- fail to load on some more exotic platforms.
+--
+--
+-- Compatibility
+-- =============
+--
+-- Based off of my testing, this Lua script is 100% compatible with the original
+-- C-based "extractbb" program, with the following exceptions:
+--
+-- * When running in "img" mode, the PDF version is always reported as "1.5".
+--
+-- * When running in "img" mode, if the requested bounding box is not found,
+-- the script will fallback to the Crop box or the Media box, instead of
+-- following the original fallback order. (In practice, almost all PDFs set
+-- all their bounding boxes equal to each other, and even if the boxes are
+-- set to different values, the script will still return the requested box,
+-- provided that it is set in the PDF.)
+--
+-- * When running in "pdfe" mode, only PDF files are supported.
+--
+-- All of these issues are very unlikely to affect any real-world documents.
+--
+--
+-- Security
+-- ========
+--
+-- This script is designed to be safely ran from restricted shell escape. A few
+-- security features:
+--
+-- * The majority of this script runs inside a sandboxed Lua environment,
+-- which only exposes a very restricted set of functions.
+--
+-- * All file-related functions available inside the sandbox first check with
+-- kpathsea to ensure that the file is allowed to be opened.
+--
+-- * In the event of any errors, the script immediately exits.
+--
+-- * This script does not run (fork/exec) any external programs.
+--
+-- * This script is written entirely in Lua, so overflow/use-after-free
+-- vulnerabilities are not possible.
+--
+-- Some potential security concerns:
+--
+-- * This script has not been audited or reviewed by anyone other than myself.
+--
+-- * Using the "ffi" module to load the "img" library is technically undefined
+-- behaviour, and as such may potentially lead to unforeseen security
+-- issues.
+--
+-- * The underlying LuaTeX modules may themselves have security
+-- vulnerabilities, which would be inherited by this script.
+
+
+----------------------
+--- Initialization ---
+----------------------
+
+-- Pre-sandbox variables/constants
+local show_errors = true
+local SOURCE_DATE_EPOCH = tonumber(os.getenv("SOURCE_DATE_EPOCH"))
+local version = "extractbb.lua v1.0.0 (2024-11-17)" --%%version %%dashdate
+
+-- Required for any kpathsea calls to work.
+kpse.set_program_name("texlua", "extractbb")
+
+
+--------------------------
+--- Questionable Hacks ---
+--------------------------
+
+-- LuaTeX doesn't load the "img" library in "texlua" mode, so we need this
+-- questionable hack to load it manually. We do it inside of "pcall" since there
+-- are some exotic platforms where the "ffi" module is unsupported.
+pcall(function()
+ local ffi = package.loaded.ffi
+
+ ffi.cdef[[
+ typedef struct lua_State lua_State;
+ typedef int (*lua_CFunction) (lua_State *L);
+
+ lua_State *Luas;
+ void luaL_requiref(lua_State *L, const char *modname,
+ lua_CFunction openf, int glb);
+ int luaopen_img(lua_State * L);
+
+ int lua_only;
+ ]]
+
+ -- Basic initialization
+ ffi.C.lua_only = 0
+ tex.initialize()
+
+ -- "tex" module
+ _G.tex = package.loaded.tex
+ tex.enableprimitives("", tex.extraprimitives())
+ tex.outputmode = 1
+ tex.interactionmode = 0
+
+ -- "pdf" module
+ _G.pdf = package.loaded.pdf
+ pdf.setignoreunknownimages(1)
+ pdf.setmajorversion(2)
+ pdf.setminorversion(0)
+
+ -- "img" module
+ ffi.C.luaL_requiref(ffi.C.Luas, "img", ffi.C.luaopen_img, 1)
+end)
+
+-- In case of failure, define an empty "img" table.
+if not img then
+ _G.img = {}
+end
+
+
+------------------
+--- Sandboxing ---
+------------------
+
+-- Prepare the sandbox for the rest of the script.
+local env = {
+ arg = arg,
+ io = { stdout = io.stdout, },
+ ipairs = ipairs,
+ math = math,
+ os = { date = os.date, exit = os.exit, },
+ pairs = pairs,
+ pdfe = pdfe,
+ print = print,
+ select = select,
+ table = table,
+ tonumber = tonumber,
+ type = type,
+}
+
+do
+ -- Saved global functions
+ local debug_traceback = debug.traceback
+ local find_file = kpse.find_file
+ local img_scan = img.scan
+ local io_open = io.open
+ local io_stderr = io.stderr
+ local kpse_in_name_ok = kpse.in_name_ok
+ local kpse_out_name_ok = kpse.out_name_ok
+ local kpse_var_value = kpse.var_value
+ local lfs_attributes = lfs.attributes
+ local os_exit = os.exit
+ local os_setenv = os.setenv
+ local pdfe_open = pdfe.open
+ local select = select
+ local tostring = tostring
+
+ -- Error messages
+ local function error(...)
+ if show_errors then
+ -- Header
+ io_stderr:write("! extractbb ERROR: ")
+
+ -- Message
+ for i = 1, select("#", ...) do
+ io_stderr:write(tostring(select(i, ...)), " ")
+ end
+
+ -- Traceback
+ io_stderr:write("\n", "\n")
+ io_stderr:write(debug_traceback(nil, 2), "\n")
+ end
+
+ -- Flush and exit
+ io_stderr:flush()
+ os_exit(1)
+ end
+
+ env.error = error
+
+ -- Make sure that "openin_any" is at least "restricted", and that
+ -- "openout_any" is at least "paranoid".
+ local initial_openin = kpse_var_value("openin_any")
+ local initial_openout = kpse_var_value("openout_any")
+
+ if (initial_openin ~= "r") or (initial_openout ~= "p") then
+ os_setenv("openin_any", "r")
+ end
+
+ if (initial_openout ~= "p") then
+ os_setenv("openout_any", "p")
+ end
+
+ -- Check the input paths.
+ local function resolve_input_name(file_name)
+ local file_path = find_file(file_name, "graphic/figure", true)
+ if not file_path then
+ error("Cannot find input file:", file_name)
+ end
+
+ local allowed = kpse_in_name_ok(file_path)
+ if not allowed then
+ error("Input file is not allowed:", file_path)
+ end
+
+ local mode = lfs_attributes(file_path, "mode")
+ if mode ~= "file" then
+ error("Input file is not a regular file:", file_path)
+ end
+
+ return file_path
+ end
+
+ -- Check the output paths.
+ local function resolve_output_name(file_name)
+ local allowed = kpse_out_name_ok(file_name)
+ if not allowed then
+ error("Output file is not allowed:", file_name)
+ end
+
+ local name, extension = file_name:match("(.+)%.([^.]-)$")
+
+ if (not name) or (not extension) or
+ (name == "") or (extension == "")
+ then
+ error("Output file has no extension:", file_name)
+ end
+
+ if (extension ~= "xbb") and (extension ~= "bb") then
+ error("Output file has an invalid extension:", file_name)
+ end
+
+ -- We shouldn't allow files with weird characters in their names.
+ if name:match("[%c%%\t\r\n><*|]") then
+ error("Output file has an invalid name:", file_name)
+ end
+
+ return file_name
+ end
+
+ -- Opens a file.
+ function env.open_file(file_name, read_write, binary_text)
+ local file_path, mode
+ if read_write == "read" then
+ file_path = resolve_input_name(file_name)
+ mode = "r"
+ elseif read_write == "write" then
+ file_path = resolve_output_name(file_name)
+ mode = "w"
+ else
+ error("Invalid read/write mode:", read_write)
+ end
+
+ if binary_text == "binary" then
+ mode = mode .. "b"
+ elseif binary_text == "text" then
+ mode = mode .. ""
+ else
+ error("Invalid binary/text mode:", binary_text)
+ end
+
+ local file, message = io_open(file_path, mode)
+
+ if not file then
+ error("Cannot open file:", file_path, message)
+ end
+
+ return file
+ end
+
+ -- Open an PDF file.
+ function env.pdfe.open(file_name)
+ local file_path = resolve_input_name(file_name)
+ return pdfe_open(file_path)
+ end
+
+ -- Open an image file.
+ function env.open_image(file_name, page, box)
+ local file_path = resolve_input_name(file_name)
+ return img_scan {
+ filename = file_path,
+ filepath = file_path,
+ page = page,
+ pagebox = box,
+ }
+ end
+
+ if not img_scan then
+ env.open_image = false
+ end
+end
+
+-- Prevent trying to change the environment.
+local function bad_index(...)
+ env.error("Attempt to access an undefined index:", select(2, ...))
+end
+
+setmetatable(env, {
+ __index = bad_index,
+ __metatable = false,
+ __newindex = bad_index,
+})
+
+-- Set the environment.
+_ENV = env
+
+
+-----------------------------------
+--- Post-Sandbox Initialization ---
+-----------------------------------
+
+-- Constants
+local BP_TO_SP = 65781.76
+local IN_TO_BP = 72
+
+-- Save often-used globals for a slight speed boost.
+local floor = math.floor
+local insert = table.insert
+local remove = table.remove
+local script_arguments = arg
+local unpack = table.unpack
+
+-- General-purpose functions
+local function round(number)
+ return floor(number +0.5)
+end
+
+
+-------------------------
+--- Argument Handling ---
+-------------------------
+
+-- Define the argument handling functions.
+local process_arguments = {}
+
+-- > Specify a PDF pagebox for bounding box
+-- > pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+local bbox_option = "auto"
+function process_arguments.B(script_arguments)
+ bbox_option = remove(script_arguments, 1)
+end
+
+-- > Show this help message and exit
+function process_arguments.h(script_arguments)
+ print [[
+Usage: extractbb [-B pagebox] [-p page] [-q|-v] [-O] [-m|-x] FILE...
+ extractbb --help|--version
+Extract bounding box from PDF, PNG, JPEG, JP2, or BMP file; default output below.
+
+Options:
+ -B pagebox Specify a PDF pagebox for bounding box
+ pagebox=cropbox, mediabox, artbox, trimbox, bleedbox
+ -h | --help Show this help message and exit
+ --version Output version information and exit
+ -p page Specify a PDF page to extract bounding box
+ -q Be quiet
+ -v Be verbose
+ -O Write output to stdout
+ -m Output .bb file used in DVIPDFM (default)
+ -x Output .xbb file used in DVIPDFMx
+]]
+ os.exit(0)
+end
+
+process_arguments["-help"] = process_arguments.h
+
+-- > Output version information and exit
+function process_arguments.V(script_arguments)
+ print(version)
+ os.exit(0)
+end
+
+process_arguments["-version"] = process_arguments.V
+
+-- > Specify a PDF page to extract bounding box
+local page_number = 1
+function process_arguments.p(script_arguments)
+ page_number = tonumber(remove(script_arguments, 1))
+end
+
+-- > Be quiet
+function process_arguments.q(script_arguments)
+ show_errors = false
+end
+
+-- > Be verbose
+function process_arguments.v(script_arguments)
+ show_errors = true
+end
+
+-- > Write output to stdout
+local output_file
+function process_arguments.O(script_arguments)
+ output_file = io.stdout
+end
+
+-- Output format
+local output_format = "xbb"
+
+if script_arguments[0]:match("ebb") then
+ output_format = "bb"
+end
+
+-- > Output .bb file used in DVIPDFM (default)
+function process_arguments.m(script_arguments)
+ output_format = "bb"
+end
+
+-- > Output .xbb file used in DVIPDFMx
+function process_arguments.x(script_arguments)
+ output_format = "xbb"
+end
+
+-- Get the input file name.
+local input_name
+function process_arguments.i(script_arguments)
+ input_name = remove(script_arguments, 1)
+end
+
+process_arguments["-input-name"] = process_arguments.i
+
+-- Clear the interpreter and script names.
+script_arguments[-1] = nil
+script_arguments[0] = nil
+
+-- Process the arguments.
+while script_arguments[1] do
+ -- Get the next argument.
+ local arg = remove(script_arguments, 1)
+ local cmd = arg:match("^%-(.*)$")
+
+ -- Default to "--input-name" if no command is given.
+ if not cmd then
+ insert(script_arguments, 1, arg)
+ cmd = "-input-name"
+ end
+
+ -- Handle multi-character arguments.
+ if (cmd:len() >= 2) and (not cmd:match("^%-")) then
+ local i = 0
+ for char in cmd:gmatch(".") do
+ i = i + 1
+ insert(script_arguments, i, "-" .. char)
+ end
+
+ goto continue
+ end
+
+ -- Get the function to process the argument and run it.
+ local func = process_arguments[cmd]
+
+ if not func then
+ error("Invalid argument:", arg)
+ end
+
+ func(script_arguments)
+
+ ::continue::
+end
+
+-- Validate the arguments.
+if not type(page_number) == "number" then
+ error("Invalid page number:", page_number)
+end
+
+if not input_name then
+ error("No input file specified.")
+end
+
+-- Validate the bounding box type. We need this rather crazy fallback scheme
+-- to match the behaviour of "extractbb".
+local bbox_orders = {}
+bbox_orders.mediabox = {
+ { img = "media", pdfe = "MediaBox" },
+}
+bbox_orders.cropbox = {
+ { img = "crop", pdfe = "CropBox" }, unpack(bbox_orders.mediabox)
+}
+bbox_orders.artbox = {
+ { img = "art", pdfe = "ArtBox" }, unpack(bbox_orders.cropbox)
+}
+bbox_orders.trimbox = {
+ { img = "trim", pdfe = "TrimBox" }, unpack(bbox_orders.artbox)
+}
+bbox_orders.bleedbox = {
+ { img = "bleed", pdfe = "BleedBox" }, unpack(bbox_orders.trimbox)
+}
+bbox_orders.auto = {
+ bbox_orders.cropbox[1], bbox_orders.artbox[1], bbox_orders.trimbox[1],
+ bbox_orders.bleedbox[1], bbox_orders.mediabox[1],
+}
+
+local bbox_order = bbox_orders[bbox_option]
+
+if not bbox_order then
+ error("Invalid PDF box type:", bbox_option)
+end
+
+-- Set the default pixel resolution.
+local default_dpi
+if output_format == "xbb" then
+ default_dpi = 72
+elseif output_format == "bb" then
+ default_dpi = 100
+else
+ error("Invalid output format:", output_format)
+end
+
+-- Open the output file.
+if not output_file then
+ local base_name = input_name:match("(.+)%.([^.]-)$") or input_name
+ local output_name = base_name .. "." .. output_format
+ output_file = open_file(output_name, "write", "text")
+end
+
+
+------------------------
+--- Image Processing ---
+------------------------
+
+local x_min, y_min, x_max, y_max
+local num_pages, image_type
+local pdf_major_version, pdf_minor_version
+
+if open_image then
+ -- Check the number of pages.
+ local image = open_image(input_name)
+ num_pages = image.pages
+
+ if page_number > num_pages then
+ error("Invalid page number:", page_number)
+ end
+
+ -- Open the image to the specified page and bounding box. If the requested
+ -- bounding box is not available, LuaTeX will fall back to the crop box
+ -- or the media box.
+ image = open_image(input_name, page_number, bbox_order[1].img)
+
+ if not image then
+ error("Cannot open image:", input_name)
+ end
+
+ -- Get the image metadata.
+ image_type = image.imagetype
+ local bounding_box = image.bbox
+
+ if not bounding_box then
+ error("Cannot get bounding box:", page_number)
+ end
+
+ local x_resolution = image.xres
+ local y_resolution = image.yres
+
+ if (x_resolution or 0) == 0 then
+ x_resolution = default_dpi
+ end
+
+ if (y_resolution or 0) == 0 then
+ y_resolution = default_dpi
+ end
+
+ -- Convert the bounding box to PostScript points.
+ for i, dimen in ipairs(bounding_box) do
+ if image_type == "pdf" then
+ dimen = dimen / BP_TO_SP
+ else
+ if i % 2 == 1 then
+ dimen = dimen / x_resolution * IN_TO_BP
+ else
+ dimen = dimen / y_resolution * IN_TO_BP
+ end
+ end
+
+ bounding_box[i] = dimen
+ end
+
+ -- Save the bounding box.
+ x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+ -- We can't get the PDF version with the "img" library, so we'll just
+ -- pretend that it's v1.5 (which supports most features).
+ pdf_major_version = 1
+ pdf_minor_version = 5
+else
+ -- Fallback to PDFs only.
+ image_type = "pdf"
+ local document = pdfe.open(input_name)
+
+ if pdfe.getstatus(document) ~= 0 then
+ error("Cannot open PDF file:", input_name)
+ end
+
+ -- Check the number of pages.
+ num_pages = pdfe.getnofpages(document)
+
+ if type(num_pages) ~= "number" then
+ error("Invalid number of pages:", num_pages)
+ end
+
+ if page_number > num_pages then
+ error("Invalid page number:", page_number)
+ end
+
+ -- Get the page.
+ local page = pdfe.getpage(document, page_number)
+
+ if not page then
+ error("Cannot get page:", page_number)
+ end
+
+ -- Get the bounding box. Here, we check the boxes in the exact same order
+ -- that "extractbb" does.
+ local bounding_box
+ for _, bbox in ipairs(bbox_order) do
+ bounding_box = pdfe.getbox(page, bbox.pdfe)
+
+ if bounding_box then
+ break
+ end
+ end
+
+ if not bounding_box then
+ error("Cannot get bounding box:", page_number)
+ end
+
+ -- Save the bounding box.
+ x_min, y_min, x_max, y_max = unpack(bounding_box)
+
+ -- Get the PDF version.
+ pdf_major_version, pdf_minor_version = pdfe.getversion(document)
+end
+
+-- Validate the bounding box.
+for _, dimen in ipairs { x_min, y_min, x_max, y_max } do
+ if type(dimen) ~= "number" then
+ error("Invalid bounding box:", x_min, y_min, x_max, y_max)
+ end
+end
+
+
+--------------
+--- Output ---
+--------------
+
+-- Get the output fields and values.
+local lines = {}
+
+insert(lines, ("Title: %s"):format(input_name))
+insert(lines, ("Creator: %s"):format(version))
+insert(lines,
+ ("BoundingBox: %d %d %d %d")
+ :format(round(x_min), round(y_min), round(x_max), round(y_max)))
+
+if output_format == "xbb" then
+ insert(lines,
+ ("HiResBoundingBox: %0.6f %0.6f %0.6f %0.6f")
+ :format(x_min, y_min, x_max, y_max))
+
+ if image_type == "pdf" then
+ insert(lines,
+ ("PDFVersion: %d.%d")
+ :format(pdf_major_version, pdf_minor_version))
+
+ insert(lines, ("Pages: %d"):format(num_pages))
+ end
+
+end
+
+insert(lines, ("CreationDate: %s"):format(os.date("%c", SOURCE_DATE_EPOCH)))
+
+-- Create the output text.
+local begin_line = "%%"
+local end_line = "\n"
+
+local text = begin_line ..
+ table.concat(lines, end_line .. begin_line) ..
+ end_line .. end_line
+
+-- Write the output text.
+output_file:write(text)
+output_file:close()
+
+-- Everything is done, so now we can exit.
+os.exit(0)