diff options
Diffstat (limited to 'Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua')
-rwxr-xr-x | Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua | 695 |
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) |