Compare commits

...

14 Commits

Author SHA1 Message Date
Alex Turchyn d4e94ac2bc
Merge from docusealco/wip
3 weeks ago
Pete Matsyburka 75bc789bb1 add reply to
3 weeks ago
Pete Matsyburka bc231d5bf1 add reply to
3 weeks ago
Alex Turchyn ab9008b3be
fix brakeman SQL injection warning
3 weeks ago
Pete Matsyburka 65491936c3 fix archived export
3 weeks ago
Pete Matsyburka e589998840 add field type class
3 weeks ago
Pete Matsyburka 0afba8d11e adjust with prefillable
3 weeks ago
Pete Matsyburka f622ed168f fix autocomplete
3 weeks ago
Pete Matsyburka 5a31df40d8 add field settings classes
3 weeks ago
Pete Matsyburka db3b80c96c fix clone
3 weeks ago
Pete Matsyburka bd28a761d0 adjust bmp loader
3 weeks ago
Pete Matsyburka 4f2830a34d increase image resolution
3 weeks ago
Pete Matsyburka a5716cd518 show html textarea
3 weeks ago
Pete Matsyburka d035525c7a additional terms
3 weeks ago

@ -48,7 +48,7 @@ ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0 ENV LD_PRELOAD=/lib/libgcompat.so.0
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
ENV VIPS_MAX_COORD=10000 ENV VIPS_MAX_COORD=15000
WORKDIR /app WORKDIR /app
@ -80,7 +80,7 @@ COPY --chown=docuseal:docuseal ./log ./log
COPY --chown=docuseal:docuseal ./lib ./lib COPY --chown=docuseal:docuseal ./lib ./lib
COPY --chown=docuseal:docuseal ./public ./public COPY --chown=docuseal:docuseal ./public ./public
COPY --chown=docuseal:docuseal ./tmp ./tmp COPY --chown=docuseal:docuseal ./tmp ./tmp
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./ COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal .version ./public/version COPY --chown=docuseal:docuseal .version ./public/version
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts

@ -0,0 +1,5 @@
Additional Terms
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the original DocuSeal attribution in interactive
user interfaces.

@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
## License ## License
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information. Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC. Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
## Tools ## Tools

@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
load_and_authorize_resource :submission, through: :template, parent: false, only: :index load_and_authorize_resource :submission, through: :template, parent: false, only: :index
def index def index
submissions = @submissions.active submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob }) submissions = submissions.preload(submitters: { documents_attachments: :blob,
.order(id: :asc) attachments_attachments: :blob })
.order(id: :asc)
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true) submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params) submissions = Submissions::Filter.call(submissions, current_user, params)

@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
LIMIT = 100 LIMIT = 100
def index def index
submitters = search_submitters(@submitters) field = SELECT_COLUMNS.find { |c| c == params[:field] }
submitters = search_submitters(@submitters, field)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] } arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
values =
if field
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
else
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
end
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs render json: attrs
end end
private private
def search_submitters(submitters) def search_submitters(submitters, field)
if SELECT_COLUMNS.include?(params[:field]) if field
if Docuseal.fulltext_search? if Docuseal.fulltext_search?
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field]) Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
else else
column = Submitter.arel_table[params[:field].to_sym] column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%" term = "#{params[:q].downcase}%"

@ -21,7 +21,7 @@ class TemplatesCloneController < ApplicationController
authorize!(:create, @template) authorize!(:create, @template)
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id])) if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
@template.account_id = params[:account_id] @template.account_id = params[:account_id]
@template.author = true_user if true_user.account_id == @template.account_id @template.author = true_user if true_user.account_id == @template.account_id
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id @template.folder = @template.account.default_template_folder if @template.account_id != current_account.id

@ -155,12 +155,15 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.appElem.classList.add('md:h-screen') this.appElem.classList.add('md:h-screen')
const template = reactive(JSON.parse(this.dataset.template))
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withPrefillable: template.fields?.some((f) => f.prefillable),
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null, withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null, withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',

@ -36,15 +36,11 @@ function loadTiptap () {
} }
class LinkTooltip { class LinkTooltip {
constructor (container, editor) { constructor (container, editor, templateEl) {
this.container = container this.container = container
this.editor = editor this.editor = editor
const template = document.createElement('template') this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
template.innerHTML = container.dataset.linkTooltipHtml
this.tooltip = template.content.firstElementChild
this.input = this.tooltip.querySelector('input') this.input = this.tooltip.querySelector('input')
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]') this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
@ -140,7 +136,8 @@ export default actionable(targetable(class extends HTMLElement {
'boldButton', 'boldButton',
'italicButton', 'italicButton',
'underlineButton', 'underlineButton',
'linkButton' 'linkButton',
'linkTooltipTemplate'
] ]
async connectedCallback () { async connectedCallback () {
@ -256,7 +253,7 @@ export default actionable(targetable(class extends HTMLElement {
} }
}) })
this.linkTooltip = new LinkTooltip(this, this.editor) this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
} }
adjustShortcutsForPlatform () { adjustShortcutsForPlatform () {

@ -720,6 +720,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withPrefillable: {
type: Boolean,
required: false,
default: false
},
customFields: { customFields: {
type: Array, type: Array,
required: false, required: false,
@ -948,13 +953,6 @@ export default {
!submitter.email !submitter.email
}) })
}, },
withPrefillable () {
if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable)
} else {
return false
}
},
isInlineSize () { isInlineSize () {
return CSS.supports('container-type: size') return CSS.supports('container-type: size')
}, },

@ -1,6 +1,7 @@
<template> <template>
<div <div
class="list-field group" class="list-field group"
:class="`list-field-${field.type}`"
> >
<div <div
class="border border-base-300 rounded relative group fields-list-item" class="border border-base-300 rounded relative group fields-list-item"

@ -11,7 +11,7 @@
> >
<label <label
v-if="showRequired" v-if="showRequired"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-required w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -26,7 +26,7 @@
</label> </label>
<label <label
v-if="showReadOnly" v-if="showReadOnly"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-read-only w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -41,7 +41,7 @@
</label> </label>
<label <label
v-if="showPrefillable" v-if="showPrefillable"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-prefillable w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -56,7 +56,7 @@
</label> </label>
<label <label
v-if="showSetSigningDate" v-if="showSetSigningDate"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-set-signing-date w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -70,7 +70,7 @@
</label> </label>
<label <label
v-if="showWithLogo" v-if="showWithLogo"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-with-logo w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -84,7 +84,7 @@
</label> </label>
<label <label
v-if="showSignatureId" v-if="showSignatureId"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-signature-id w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -99,7 +99,7 @@
</label> </label>
<label <label
v-if="showChecked" v-if="showChecked"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-checked w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop @click.stop
> >
<input <input
@ -113,6 +113,7 @@
</label> </label>
<ContextSubmenu <ContextSubmenu
v-if="showVerificationMethod" v-if="showVerificationMethod"
class="field-settings-verification-method"
:icon="IconId" :icon="IconId"
:label="t('method')" :label="t('method')"
:options="methodOptions" :options="methodOptions"
@ -125,6 +126,7 @@
> >
<ContextSubmenu <ContextSubmenu
v-if="showFormatSubmenu" v-if="showFormatSubmenu"
class="field-settings-format"
:icon="IconAdjustmentsHorizontal" :icon="IconAdjustmentsHorizontal"
:label="t('format')" :label="t('format')"
:options="formatOptions" :options="formatOptions"
@ -133,6 +135,7 @@
/> />
<ContextSubmenu <ContextSubmenu
v-if="showValidationSubmenu && field.type !== 'number'" v-if="showValidationSubmenu && field.type !== 'number'"
class="field-settings-validation"
:icon="IconInputCheck" :icon="IconInputCheck"
:label="t('validation')" :label="t('validation')"
:options="validationMenuItems.map(k => ({ value: k, label: t(k) }))" :options="validationMenuItems.map(k => ({ value: k, label: t(k) }))"
@ -141,7 +144,7 @@
/> />
<button <button
v-if="field.type === 'number'" v-if="field.type === 'number'"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm" class="field-settings-validation w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openNumberRangeModal" @click.stop="openNumberRangeModal"
> >
<IconInputCheck class="w-4 h-4" /> <IconInputCheck class="w-4 h-4" />
@ -149,6 +152,7 @@
</button> </button>
<ContextSubmenu <ContextSubmenu
v-if="showPaymentSettings" v-if="showPaymentSettings"
class="field-settings-currency"
:icon="IconCash" :icon="IconCash"
:label="t('currency')" :label="t('currency')"
:options="currencyOptions" :options="currencyOptions"
@ -157,6 +161,7 @@
/> />
<ContextSubmenu <ContextSubmenu
v-if="showPaymentSettings" v-if="showPaymentSettings"
class="field-settings-price"
:icon="IconCoins" :icon="IconCoins"
:label="t('price')" :label="t('price')"
:options="priceTypeOptions" :options="priceTypeOptions"
@ -165,7 +170,7 @@
/> />
<button <button
v-if="showFont" v-if="showFont"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm" class="field-settings-font w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFontModal" @click.stop="openFontModal"
> >
<IconTypography class="w-4 h-4" /> <IconTypography class="w-4 h-4" />
@ -173,7 +178,7 @@
</button> </button>
<button <button
v-if="showDescription" v-if="showDescription"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm" class="field-settings-description w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openDescriptionModal" @click.stop="openDescriptionModal"
> >
<IconInfoCircle class="w-4 h-4" /> <IconInfoCircle class="w-4 h-4" />
@ -181,7 +186,7 @@
</button> </button>
<button <button
v-if="showCondition" v-if="showCondition"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm" class="field-settings-condition w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="openConditionModal" @click.stop="openConditionModal"
> >
<span class="flex items-center space-x-2"> <span class="flex items-center space-x-2">
@ -196,7 +201,7 @@
</button> </button>
<button <button
v-if="showFormula" v-if="showFormula"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm" class="field-settings-formula w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFormulaModal" @click.stop="openFormulaModal"
> >
<IconMathFunction class="w-4 h-4" /> <IconMathFunction class="w-4 h-4" />
@ -207,7 +212,7 @@
class="my-1 border-neutral-200" class="my-1 border-neutral-200"
> >
<button <button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm" class="field-settings-copy w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="$emit('copy')" @click.stop="$emit('copy')"
> >
<span class="flex items-center space-x-2"> <span class="flex items-center space-x-2">
@ -217,7 +222,7 @@
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span> <span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
</button> </button>
<button <button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600" class="field-settings-remove w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
@click.stop="$emit('delete')" @click.stop="$emit('delete')"
> >
<span class="flex items-center space-x-2"> <span class="flex items-center space-x-2">
@ -228,12 +233,13 @@
</button> </button>
<ContextSubmenu <ContextSubmenu
v-if="showMoreSubmenu" v-if="showMoreSubmenu"
class="field-settings-more"
:icon="IconDots" :icon="IconDots"
:label="t('more')" :label="t('more')"
> >
<button <button
v-if="showDrawNewArea" v-if="showDrawNewArea"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-draw-new-area w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('draw_new_area')" @click="handleMoreSelect('draw_new_area')"
> >
<IconNewSection class="w-4 h-4" /> <IconNewSection class="w-4 h-4" />
@ -241,7 +247,7 @@
</button> </button>
<button <button
v-if="showCopyToAllPages" v-if="showCopyToAllPages"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-copy-to-all-pages w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('copy_to_all_pages')" @click="handleMoreSelect('copy_to_all_pages')"
> >
<IconCopy class="w-4 h-4" /> <IconCopy class="w-4 h-4" />
@ -249,7 +255,7 @@
</button> </button>
<button <button
v-if="showSaveAsCustom" v-if="showSaveAsCustom"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer" class="field-settings-save-as-custom-field w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('save_as_custom')" @click="handleMoreSelect('save_as_custom')"
> >
<IconForms class="w-4 h-4" /> <IconForms class="w-4 h-4" />

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="field.type === 'verification'" v-if="field.type === 'verification'"
class="py-1.5 px-1 relative" class="field-settings-verification-method py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -28,7 +28,7 @@
</div> </div>
<div <div
v-if="['select', 'radio'].includes(field.type) && !defaultField" v-if="['select', 'radio'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative" class="field-settings-default-value py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -62,7 +62,7 @@
</div> </div>
<div <div
v-if="['text', 'number'].includes(field.type) && !defaultField" v-if="['text', 'number'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative" class="field-settings-default-value py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@ -84,7 +84,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type)" v-if="['text', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative" class="field-settings-validation py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -122,7 +122,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
class="py-1.5 px-1 relative flex space-x-1" class="field-settings-length-validation py-1.5 px-1 relative flex space-x-1"
@click.stop @click.stop
> >
<div class="w-1/2 relative"> <div class="w-1/2 relative">
@ -166,7 +166,7 @@
</div> </div>
<div <div
v-if="field.type === 'number'" v-if="field.type === 'number'"
class="py-1.5 px-1 relative flex space-x-1" class="field-settings-number-range py-1.5 px-1 relative flex space-x-1"
@click.stop @click.stop
> >
<div class="w-1/2 relative"> <div class="w-1/2 relative">
@ -210,7 +210,7 @@
</div> </div>
<div <div
v-if="field.type === 'number'" v-if="field.type === 'number'"
class="py-1.5 px-1 relative" class="field-settings-number-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -237,7 +237,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative" class="field-settings-custom-validation py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@ -259,7 +259,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative" class="field-settings-error-message py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@ -280,7 +280,7 @@
</div> </div>
<div <div
v-if="field.type === 'date'" v-if="field.type === 'date'"
class="py-1.5 px-1 relative" class="field-settings-date-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -307,7 +307,7 @@
</div> </div>
<div <div
v-if="field.type === 'signature'" v-if="field.type === 'signature'"
class="py-1.5 px-1 relative" class="field-settings-signature-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -340,6 +340,7 @@
</div> </div>
<li <li
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'" v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
class="field-settings-signature-id"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -355,6 +356,7 @@
</li> </li>
<li <li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'" v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
class="field-settings-required"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -370,6 +372,7 @@
</li> </li>
<li <li
v-if="field.type == 'stamp'" v-if="field.type == 'stamp'"
class="field-settings-with-logo"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -384,6 +387,7 @@
</li> </li>
<li <li
v-if="field.type == 'checkbox'" v-if="field.type == 'checkbox'"
class="field-settings-checked"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -398,6 +402,7 @@
</li> </li>
<li <li
v-if="field.type == 'date'" v-if="field.type == 'date'"
class="field-settings-set-signing-date"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -412,6 +417,7 @@
</li> </li>
<li <li
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)" v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
class="field-settings-read-only"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -427,6 +433,7 @@
</li> </li>
<li <li
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])" v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
class="field-settings-prefillable"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@ -444,7 +451,10 @@
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
> >
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"> <li
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"
class="field-settings-font"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')" @click="$emit('click-font')"
@ -459,6 +469,7 @@
</li> </li>
<li <li
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'" v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
class="field-settings-description"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@ -474,6 +485,7 @@
</li> </li>
<li <li
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'" v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
class="field-settings-condition"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@ -487,7 +499,10 @@
</span> </span>
</label> </label>
</li> </li>
<li v-if="field.type == 'number'"> <li
v-if="field.type == 'number'"
class="field-settings-formula"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')" @click="$emit('click-formula')"
@ -508,6 +523,7 @@
<li <li
v-for="(area, index) in sortedAreas" v-for="(area, index) in sortedAreas"
:key="index" :key="index"
class="field-settings-area"
> >
<a <a
href="#" href="#"
@ -527,7 +543,10 @@
/> />
</a> </a>
</li> </li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"> <li
v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"
class="field-settings-draw-new-area"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@ -541,7 +560,10 @@
</a> </a>
</li> </li>
</template> </template>
<li v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"> <li
v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"
class="field-settings-copy-to-all-pages"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@ -554,7 +576,10 @@
{{ t('copy_to_all_pages') }} {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
<li v-if="withCustomFields"> <li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"

@ -22,7 +22,7 @@
> >
<div <div
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)" v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
class="py-1.5 px-1 relative" class="field-settings-currency py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@ -48,7 +48,7 @@
</label> </label>
</div> </div>
<div <div
class="py-1.5 px-1 relative" class="field-settings-price py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@ -121,7 +121,7 @@
</div> </div>
<div <div
v-if="!isConnected || isOauthSuccess" v-if="!isConnected || isOauthSuccess"
class="py-1.5 px-1 relative" class="field-settings-stripe-connect py-1.5 px-1 relative"
@click.stop @click.stop
> >
<div <div
@ -203,7 +203,7 @@
>{{ t('learn_more') }}</a> >{{ t('learn_more') }}</a>
</div> </div>
<li <li
class="mb-1" class="field-settings-formula mb-1"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@ -218,7 +218,7 @@
</label> </label>
</li> </li>
<hr> <hr>
<li> <li class="field-settings-description">
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-description')" @click="$emit('click-description')"
@ -233,7 +233,7 @@
</li> </li>
<li <li
v-if="withCondition" v-if="withCondition"
class="mt-1" class="field-settings-condition mt-1"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@ -251,7 +251,10 @@
v-if="withCustomFields" v-if="withCustomFields"
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
> >
<li v-if="withCustomFields"> <li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"

@ -29,7 +29,7 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_invitation', @submitter) assign_message_metadata('submitter_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter) reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
maybe_set_custom_domain(@submitter) maybe_set_custom_domain(@submitter)

@ -1,70 +1,76 @@
<% link_tooltip_html = capture do %> <% if value.to_s.start_with?('<html') %>
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false"> <autoresize-textarea>
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off"> <%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer"> </autoresize-textarea>
<%= svg_icon('check', class: 'w-4 h-4 text-success') %> <% else %>
</button> <markdown-editor>
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer"> <template data-target="markdown-editor.linkTooltipTemplate">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %> <div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
</button> <input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
</div> <button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<% end %> <%= svg_icon('check', class: 'w-4 h-4 text-success') %>
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>"> </button>
<div class="border border-base-content/20 rounded-2xl bg-white"> <button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;"> <%= svg_icon('x', class: 'w-4 h-4 text-error') %>
<div class="flex items-center gap-1"> </button>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div> </div>
<div class="mx-2 h-5 border-l border-base-content/20"></div> </template>
<div class="flex items-center gap-1"> <div class="border border-base-content/20 rounded-2xl bg-white">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)"> <div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <div class="flex items-center gap-1">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
</button> <button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
</div> <%= svg_icon('bold', class: 'w-4 h-4') %>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)"> </button>
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> </div>
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
</button> <button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div> </div>
</div> <div class="mx-2 h-5 border-l border-base-content/20"></div>
<% if local_assigns[:variables]&.any? %> <div class="flex items-center gap-1">
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<div class="dropdown dropdown-end ml-auto"> <button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer"> <%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
<%= t('add_variable') %> </button>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %> </div>
</label> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50"> <button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<% local_assigns[:variables]&.each do |variable| %> <%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap"> </button>
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div> </div>
</div> </div>
<% end %> <% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<div data-target="markdown-editor.editorElement"></div>
</div> </div>
<div data-target="markdown-editor.editorElement"></div> <%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</div> </markdown-editor>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %> <% end %>
</markdown-editor>

@ -8,12 +8,18 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>

@ -1,4 +1,4 @@
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %> <% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %>
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %> <%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
<div class="space-y-2"> <div class="space-y-2">
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %> <%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>

@ -16,7 +16,7 @@
<% if with_filters %> <% if with_filters %>
<%= render 'shared/search_input' %> <%= render 'shared/search_input' %>
<% end %> <% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span> <span><%= t('export') %></span>
<% end %> <% end %>

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module LoadBmp module LoadBmp
BPPS = [1, 4, 8, 24, 32].freeze
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
@ -16,18 +18,25 @@ module LoadBmp
header_data[:height] header_data[:height]
) )
final_pixel_data = prepare_unpadded_pixel_data_string( if header_data[:bpp] <= 8
raw_pixel_data_from_file, final_pixel_data = decode_indexed_pixel_data(
header_data[:bpp], raw_pixel_data_from_file,
header_data[:width], header_data[:bpp],
header_data[:height], header_data[:width],
header_data[:bmp_stride] header_data[:height],
) header_data[:bmp_stride],
header_data[:color_table]
bands = header_data[:bpp] / 8 )
bands = 3
unless header_data[:bpp] == 24 || header_data[:bpp] == 32 else
raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented." final_pixel_data = prepare_unpadded_pixel_data_string(
raw_pixel_data_from_file,
header_data[:bpp],
header_data[:width],
header_data[:height],
header_data[:bmp_stride]
)
bands = header_data[:bpp] / 8
end end
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar) image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
@ -35,7 +44,9 @@ module LoadBmp
image = image.flip(:vertical) if header_data[:orientation] == -1 image = image.flip(:vertical) if header_data[:orientation] == -1
image_rgb = image_rgb =
if bands == 3 if header_data[:bpp] <= 8
image
elsif bands == 3
image.recomb(band3_recomb) image.recomb(band3_recomb)
elsif bands == 4 elsif bands == 4
image.recomb(band4_recomb) image.recomb(band4_recomb)
@ -93,15 +104,31 @@ module LoadBmp
"Unsupported BMP compression type: #{compression}. Only uncompressed (0) is supported." "Unsupported BMP compression type: #{compression}. Only uncompressed (0) is supported."
end end
unless [24, 32].include?(bpp) if BPPS.exclude?(bpp)
raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 24-bit and 32-bit are supported." raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 1, 4, 8, 24, and 32-bit are supported."
end end
raise ArgumentError, "Unsupported BMP planes: #{planes}. Expected 1." if planes != 1 raise ArgumentError, "Unsupported BMP planes: #{planes}. Expected 1." if planes != 1
bytes_per_pixel = bpp / 8 bmp_stride = (((width * bpp) + 31) / 32) * 4
row_size_unpadded = width * bytes_per_pixel
bmp_stride = (row_size_unpadded + 3) & ~3 color_table = nil
if bpp <= 8
num_colors = 1 << bpp
color_table_offset = 14 + info_header_size
color_table_size = num_colors * 4
if bmp_bytes.bytesize < color_table_offset + color_table_size
raise ArgumentError, 'BMP data too short for color table.'
end
color_table = Array.new(num_colors) do |i|
offset = color_table_offset + (i * 4)
b, g, r = bmp_bytes.unpack("@#{offset}CCC")
[r, g, b]
end
end
{ {
width:, width:,
@ -109,7 +136,8 @@ module LoadBmp
bpp:, bpp:,
pixel_data_offset:, pixel_data_offset:,
bmp_stride:, bmp_stride:,
orientation: orientation:,
color_table:
} }
end end
@ -163,6 +191,37 @@ module LoadBmp
unpadded_rows.join unpadded_rows.join
end end
def decode_indexed_pixel_data(raw_data, bpp, width, height, bmp_stride, color_table)
palette = color_table.map { |r, g, b| [r, g, b].pack('CCC') }
output = String.new(capacity: width * height * 3)
height.times do |y|
row_offset = y * bmp_stride
case bpp
when 1
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 3))
index = (byte_val >> (7 - (x & 7))) & 0x01
output << palette[index]
end
when 4
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 1))
index = x.even? ? (byte_val >> 4) & 0x0F : byte_val & 0x0F
output << palette[index]
end
when 8
width.times do |x|
output << palette[raw_data.getbyte(row_offset + x)]
end
end
end
output
end
def band3_recomb def band3_recomb
@band3_recomb ||= @band3_recomb ||=
Vips::Image.new_from_array( Vips::Image.new_from_array(

Loading…
Cancel
Save